ken_nogi/ClaudePleasanter/営業積算システム/generate-site-documentation.js
Kenichiro NOGI ed33892f08 chore: 作業中の変更を整理しコミット(複数プロジェクト分)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 11:09:50 +09:00

646 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require("fs");
const path = require("path");
const child_process = require("child_process");
const ROOT = __dirname;
const CONFIG_PATH = path.join(ROOT, "config.json");
const CONFIGS_DIR = path.join(ROOT, "configs");
const DOCS_DIR = path.join(ROOT, "docs");
const SITESETTING_JSON_DIR = path.join(ROOT, "siteSettingJsons");
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
}
function sanitizeName(raw) {
return String(raw || "").trim()
.replace(/[\\/:*?"<>|]/g, "_")
.replace(/\s+/g, " ")
.replace(/\.+$/, "")
.slice(0, 60)
.trim();
}
function parseSiteIds(rawSiteId) {
const list = Array.isArray(rawSiteId) ? rawSiteId : String(rawSiteId).split(",");
return list.map((id) => String(id).trim()).filter((id) => id.length > 0);
}
function resolveSiteData(json) {
if (json?.Response?.Data) return json.Response.Data;
if (json?.Response?.Site?.Data) return json.Response.Site.Data;
if (json?.Response?.Data?.SiteSettings) return json.Response.Data;
if (json?.Response?.SiteSettings) return json.Response;
throw new Error("getsiteレスポンスの想定構造が見つかりません。Response.Data を含むJSONを渡してください。");
}
function resolveTableCellValue(row, header) {
if (row[header] != null) return row[header];
const normalized = String(header)
.replace(/\s+/g, "")
.replace(/[]/g, "")
.replace(//g, "/");
if (row[normalized] != null) return row[normalized];
const aliasMap = {
"順番": ["No", "Order", "Index"],
"列名": ["Column", "ColumnName", "Field", "Name"],
"列名(内部)": ["Column", "ColumnName", "Field", "Name"],
"表示ラベル": ["Label", "LabelText", "DisplayName", "Title"],
"説明/入力ガイド": ["Description", "DescriptionText", "ToolTip", "InputHelpText"],
"備考": ["Remarks", "Remark", "Notes"],
"種別": ["Type", "Kind"],
"対象": ["Target", "TargetId"],
"権限値": ["Permission", "PermissionValue", "Level", "Value"],
"タブ/キー": ["Tab", "Key", "TabKey"],
"項目一覧": ["Items", "ItemList"],
"項目": ["Field", "Column", "ColumnName"],
"権限設定": ["Permission", "PermissionSetting", "Value"],
"ファイル": ["File", "Path"],
"要約": ["Summary", "Description"],
"操作": ["Operation", "Name"],
"状態の変化": ["Change", "Transition"],
"説明": ["Description", "Detail"],
"起点列": ["From", "FromColumn", "FromField"],
"参照先SiteId": ["TargetSite", "TargetSiteId", "SiteId"],
"参照先の値→コピー先": ["To", "ToColumn", "ToField"],
"JsonFormat": ["JsonFormat", "Format"],
"ラベル": ["Label", "LabelText"],
"内容": ["Content", "Value", "Body"],
"参照先の列From": ["From", "FromColumn", "FromField"],
"自サイトへのコピー先To": ["To", "ToColumn", "ToField"],
"上書き設定": ["Overwrite"],
"実行条件(Current→Changed)": ["Condition"],
"動作": ["Action"],
};
for (const alias of aliasMap[header] || []) {
if (row[alias] != null) return row[alias];
}
return "";
}
function formatMarkdownTable(header, rows) {
if (!rows || rows.length === 0) return "";
const heads = header.map((h) => `| ${h} `).join("") + "|";
const sep = header.map(() => "| --- ").join("") + "|";
const body = rows
.map((row) => {
return header
.map((cell) => {
const value = resolveTableCellValue(row, cell);
return `| ${String(value).replace(/\|/g, "\\|")} `;
})
.join("") + "|";
})
.join("\n");
return `${heads}\n${sep}\n${body}\n`;
}
function inferColumnType(column) {
const name = String(column.ColumnName || column.Name || "");
if (/^(Class[A-Z]|Class\d{3}|Class)/.test(name)) return "文字列";
if (/^Num/.test(name)) return "数値";
if (/^Date/.test(name)) return "日付";
if (/^Description/.test(name)) return "説明";
if (/^Check/.test(name)) return "チェック";
if (/^Attachments?/.test(name)) return "添付";
if (/^(Owner|Manager|Assignee|CreatedBy|UpdatedBy|Updator|Users?)/.test(name)) return "ユーザー";
if (/^Status$/i.test(name)) return "状態";
if (/Id$/.test(name) && !/^Title$/i.test(name)) return "ID";
if (/Title/i.test(name)) return "タイトル";
return column.Type || column.FieldType || "不明";
}
function getArray(value) {
if (!value) return [];
return Array.isArray(value) ? value : [value];
}
function buildColumnLabelMap(columns) {
const map = {};
getArray(columns).forEach((col) => {
const name = col.ColumnName || col.Name || col.Field;
if (name) map[name] = col.LabelText || col.DisplayName || col.Title || "";
});
return map;
}
function parsePermissionEntry(perm) {
if (typeof perm === "string") {
const [type, target, level] = perm.split(",");
return { Type: type || "", Target: target || "", Level: level || "" };
}
return {
Type: formatValue(perm.Type || perm.PermissionType || perm.AuthorityType),
Target: formatValue(perm.TargetId || perm.Target || perm.RoleId || perm.Value),
Level: formatValue(perm.Permission || perm.Value || perm.Level),
};
}
function resolveGridColumnEntry(col, labelMap) {
if (typeof col === "string") {
return { Column: col, Label: labelMap[col] || "" };
}
const name = formatValue(col.ColumnName || col.Column || col.Name || col.Field);
return { Column: name, Label: formatValue(col.LabelText || col.DisplayName || col.Title || labelMap[name] || "") };
}
function objectToFieldPermissionRows(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
return Object.entries(value).map(([field, level]) => ({
Field: field,
Permission: formatValue(level),
}));
}
function formatValue(value) {
if (value === null || value === undefined) return "";
if (typeof value === "boolean") return value ? "true" : "false";
if (Array.isArray(value)) {
return value.map((item) => (item !== null && typeof item === "object" ? JSON.stringify(item) : String(item))).join(", ");
}
if (typeof value === "object") return JSON.stringify(value);
return String(value);
}
function findExtractionDir(siteId) {
if (!fs.existsSync(CONFIGS_DIR)) return null;
const candidates = fs.readdirSync(CONFIGS_DIR).filter((name) => {
const full = path.join(CONFIGS_DIR, name);
return fs.statSync(full).isDirectory() && name.startsWith(`site-${siteId}`);
});
return candidates[0] ? path.join(CONFIGS_DIR, candidates[0]) : null;
}
function loadManifest(siteId) {
const dir = findExtractionDir(siteId);
if (!dir) return null;
const manifestPath = path.join(dir, "manifest.json");
if (!fs.existsSync(manifestPath)) return null;
return readJson(manifestPath);
}
function getSiteJsonPath(siteId) {
const searchDirs = [SITESETTING_JSON_DIR, CONFIGS_DIR];
for (const dir of searchDirs) {
if (!fs.existsSync(dir)) continue;
const candidate = path.join(dir, `site-${siteId}_latest.json`);
if (fs.existsSync(candidate)) return candidate;
const all = fs.readdirSync(dir).filter((name) => name.startsWith(`site-${siteId}_`) && name.endsWith(".json"));
if (all.length > 0) return path.join(dir, all[0]);
}
return null;
}
function buildBasicInfoTable(data, settings) {
const rows = [
{ Key: "TenantId", Value: formatValue(data.TenantId) },
{ Key: "Title", Value: formatValue(data.Title) },
{ Key: "ReferenceType", Value: formatValue(data.ReferenceType) },
{ Key: "ParentId", Value: formatValue(data.ParentId) },
{ Key: "InheritPermission", Value: formatValue(data.InheritPermission) },
{ Key: "Publish", Value: formatValue(data.Publish) },
{ Key: "DisableCrossSearch", Value: formatValue(data.DisableCrossSearch) },
{ Key: "Creator", Value: formatValue(data.Creator) },
{ Key: "Updator", Value: formatValue(data.Updator || data.UpdatedBy) },
{ Key: "CreatedTime", Value: formatValue(data.CreatedTime) },
{ Key: "UpdatedTime", Value: formatValue(data.UpdatedTime) },
{ Key: "SiteSettings.Version", Value: formatValue(settings?.Version) },
];
return rows.map((row) => `| ${row.Key} | ${row.Value} |`).join("\n") + "\n";
}
function inferLinkDescription(link) {
const from = link.From || link.FromColumn || link.FromField || "(不明)";
const to = link.To || link.ToColumn || link.ToField || "(不明)";
return `${from}${to}`;
}
function scriptSummary(filePath) {
if (!fs.existsSync(filePath)) return "";
const content = fs.readFileSync(filePath, "utf-8");
const line = content.split(/\r?\n/).find((l) => l.trim().length > 0);
return line ? line.slice(0, 120) : "";
}
function generateSpecMarkdown(siteId, data, settings, manifest, siteJsonPath) {
const title = data.Title || `Site ${siteId}`;
const specLines = [];
const siteJsonRelPath = path.relative(DOCS_DIR, siteJsonPath).replace(/\\/g, "/");
specLines.push(`# サイト仕様書:${title}SiteId: ${siteId}\n`);
specLines.push(`- 取得元データ: [${path.basename(siteJsonPath)}](${encodeURI(siteJsonRelPath)})`);
specLines.push(`- 取得日時: ${formatValue(data.UpdatedTime || data.CreatedTime || "不明")}`);
specLines.push(`- 設定バージョン: ${formatValue(settings?.Version || "不明")}\n`);
specLines.push(`## 1. サイト基本情報`);
specLines.push("| 項目 | 値 |\n| --- | --- |\n" + buildBasicInfoTable(data, settings));
const permissions = getArray(settings?.Permissions).length > 0 ? getArray(settings.Permissions) : getArray(data.Permissions);
specLines.push(`## 2. アクセス権限Permissions`);
if (permissions.length > 0) {
const permRows = permissions.map((perm) => {
const { Type, Target, Level } = parsePermissionEntry(perm);
return { Type, Target, Level };
});
specLines.push(formatMarkdownTable(["種別", "対象", "権限値"], permRows));
specLines.push(`※ 権限値はビットフラグの加算方式のため、正確な意味は環境のロール定義に依存します。`);
} else if (String(data.InheritPermission) !== String(siteId)) {
specLines.push(`このサイト自体には権限設定がなく、SiteId ${formatValue(data.InheritPermission)} の権限を継承しています。継承元サイトの権限設定を別途確認してください。`);
} else {
specLines.push(`権限設定なし。`);
}
specLines.push(`## 3. 画面構成`);
const columnLabelMap = buildColumnLabelMap(settings?.Columns || []);
const gridColumns = settings?.GridColumns || [];
if (gridColumns.length > 0) {
specLines.push(`### 3.1 一覧画面GridColumns`);
const gridRows = gridColumns.map((col, index) => {
const { Column, Label } = resolveGridColumnEntry(col, columnLabelMap);
return { No: index + 1, Column, Label };
});
specLines.push(formatMarkdownTable(["順番", "列名", "表示ラベル"], gridRows));
}
const editorHash = settings?.EditorColumnHash || {};
const sections = settings?.Sections || [];
if (Object.keys(editorHash).length > 0 || sections.length > 0) {
specLines.push(`### 3.2 編集画面レイアウトEditorColumnHash / Sections`);
if (Object.keys(editorHash).length > 0) {
Object.keys(editorHash).forEach((tabKey) => {
specLines.push(`#### タブ: ${tabKey}`);
const itemRows = getArray(editorHash[tabKey]).map((item, index) => {
if (typeof item === "string" && /^_Section-\d+/.test(item)) {
const secId = item.match(/^_Section-(\d+)/)[1];
const section = sections.find((s) => String(s.Id) === secId);
return { No: index + 1, Column: item, Label: `―(セクション「${section?.LabelText || ""}」開始)` };
}
const { Column, Label } = resolveGridColumnEntry(item, columnLabelMap);
return { No: index + 1, Column, Label };
});
specLines.push(formatMarkdownTable(["順番", "列名(内部)", "表示ラベル"], itemRows));
});
}
if (sections.length > 0) {
const sectionRows = sections.map((section) => ({
Id: formatValue(section.Id),
Label: formatValue(section.LabelText || section.Name || ""),
AllowExpand: formatValue(section.AllowExpand),
Expand: formatValue(section.Expand),
}));
specLines.push(`\n#### Sections`);
specLines.push(formatMarkdownTable(["Id", "ラベル", "AllowExpand", "Expand"], sectionRows));
}
}
const titleColumns = settings?.TitleColumns || [];
if (titleColumns.length > 0) {
specLines.push(`### 3.3 タイトル表示TitleColumns / TitleSeparator`);
specLines.push(`- タイトル項目: ${formatValue(titleColumns.join(" + "))}`);
specLines.push(`- 区切り文字: ${formatValue(settings?.TitleSeparator || "")}`);
}
const columns = settings?.Columns || [];
if (columns.length > 0) {
specLines.push(`## 4. 項目定義Columns`);
const colRows = columns.map((col) => ({
Column: formatValue(col.ColumnName || col.Name || col.Field || col.Id),
Type: inferColumnType(col),
Label: formatValue(col.LabelText || col.DisplayName || col.Title || ""),
Description: formatValue(col.Description || col.ToolTip || col.InputHelpText || ""),
Remarks: [col.NoWrap ? "NoWrap" : null, col.FieldCss ? "FieldCss" : null, col.ExtendedHtmlAfterControl ? "ExtendedHtmlAfterControl" : null].filter(Boolean).join(", "),
}));
specLines.push(formatMarkdownTable(["列名(内部)", "種別", "表示ラベル", "説明/入力ガイド", "備考"], colRows));
const extendedItems = columns.filter((col) => col.ExtendedHtmlAfterControl);
if (extendedItems.length > 0) {
specLines.push(`## 4.1 項目ごとの補足HTMLExtendedHtmlAfterControl`);
const extRows = extendedItems.map((col) => ({
Column: formatValue(col.ColumnName || col.Name || col.Field),
Label: formatValue(col.LabelText || col.DisplayName || col.Title || ""),
Content: formatValue(col.ExtendedHtmlAfterControl),
}));
specLines.push(formatMarkdownTable(["列名", "ラベル", "内容"], extRows));
}
}
const createRows = objectToFieldPermissionRows(settings?.PermissionForCreating);
const updateRows = objectToFieldPermissionRows(settings?.PermissionForUpdating);
specLines.push(`## 5. 作成・更新権限(フィールド単位)`);
if (createRows.length > 0 || updateRows.length > 0) {
if (createRows.length > 0) {
specLines.push(`### PermissionForCreating`);
specLines.push(formatMarkdownTable(["項目", "権限設定"], createRows));
}
if (updateRows.length > 0) {
specLines.push(`### PermissionForUpdating`);
specLines.push(formatMarkdownTable(["項目", "権限設定"], updateRows));
}
} else {
specLines.push(`設定なし。`);
}
const aggregations = settings?.Aggregations || [];
if (aggregations.length > 0) {
specLines.push(`## 6. 集計設定Aggregations`);
const aggRows = aggregations.map((item) => ({
Id: formatValue(item.Id),
GroupBy: formatValue(item.GroupBy),
Type: formatValue(item.Type),
Target: formatValue(item.Target),
}));
specLines.push(formatMarkdownTable(["Id", "GroupBy", "Type", "Target"], aggRows));
}
const links = settings?.Links || [];
if (links.length > 0) {
specLines.push(`## 7. 他サイト連携Links / ルックアップ)`);
links.forEach((link) => {
const targetSite = formatValue(link.SiteId ?? link.TargetSiteId ?? link.ReferenceSiteId);
const targetLabel = link.TableName && Number(link.SiteId) === 0
? `マスタテーブル: ${link.TableName}`
: `参照先SiteId: ${targetSite}`;
specLines.push(`### 参照列: ${formatValue(link.ColumnName)}${targetLabel}`);
const lookups = getArray(link.Lookups);
if (lookups.length > 0) {
const lookupRows = lookups.map((lk) => ({
From: formatValue(lk.From || lk.FromColumn || lk.FromField),
To: formatValue(lk.To || lk.ToColumn || lk.ToField),
Overwrite: formatValue(lk.OverwriteForm ?? lk.Overwrite ?? ""),
}));
specLines.push(formatMarkdownTable(["参照先の列From", "自サイトへのコピー先To", "上書き設定"], lookupRows));
}
if (link.SearchFormat) specLines.push(`- 検索表示フォーマット: \`${link.SearchFormat}\``);
specLines.push(`- JsonFormat: ${formatValue(link.JsonFormat)}`);
});
}
const processes = manifest ? readJson(path.join(findExtractionDir(siteId), "processes.json")) : settings?.Processes || [];
if (processes.length > 0) {
specLines.push(`## 8. プロセス設定Processes`);
const processesRelPath = path.relative(DOCS_DIR, path.join(findExtractionDir(siteId), "processes.json")).replace(/\\/g, "/");
specLines.push(`- 定義ファイル: [${processesRelPath}](${encodeURI(processesRelPath)})`);
const procRows = processes.map((proc) => ({
Id: formatValue(proc.Id),
Name: formatValue(proc.Name),
DisplayName: formatValue(proc.DisplayName || proc.Name),
Condition: formatValue(`${proc.CurrentStatus ?? ""}${proc.ChangedStatus ?? ""}`),
Action: formatValue(proc.OnClick || proc.Action || (proc.DataChanges ? "データ変更DataChanges" : proc.ValidateInputs ? "入力必須チェックValidateInputs" : proc.Name)),
}));
specLines.push(formatMarkdownTable(["Id", "Name", "DisplayName", "実行条件(Current→Changed)", "動作"], procRows));
}
function relLinkFromDocs(subdir, fileName) {
const abs = path.join(findExtractionDir(siteId), subdir, fileName);
const rel = path.relative(DOCS_DIR, abs).replace(/\\/g, "/");
return `[${fileName}](${encodeURI(rel)})`;
}
if (manifest?.Styles?.length > 0) {
specLines.push(`## 9. スタイルStyles`);
const styleRows = manifest.Styles.map((style) => ({
Id: formatValue(style.Id),
Title: formatValue(style.Title),
File: relLinkFromDocs("styles", style.File),
Remarks: formatValue(style.Disabled ? "Disabled" : ""),
}));
specLines.push(formatMarkdownTable(["Id", "Title", "ファイル", "備考"], styleRows));
}
const scripts = manifest?.Scripts || [];
const serverScripts = manifest?.ServerScripts || [];
if (scripts.length > 0 || serverScripts.length > 0) {
specLines.push(`## 10. スクリプトScriptsサーバースクリプトServerScripts`);
if (scripts.length > 0) {
const scriptRows = scripts.map((script) => ({
Id: formatValue(script.Id),
Title: formatValue(script.Title),
File: relLinkFromDocs("scripts", script.File),
Summary: formatValue(scriptSummary(path.join(findExtractionDir(siteId), "scripts", script.File))),
}));
specLines.push(`### Scripts`);
specLines.push(formatMarkdownTable(["Id", "Title", "ファイル", "要約"], scriptRows));
}
if (serverScripts.length > 0) {
const serverRows = serverScripts.map((script) => ({
Id: formatValue(script.Id),
Title: formatValue(script.Title),
File: relLinkFromDocs("serverscripts", script.File),
Summary: formatValue(scriptSummary(path.join(findExtractionDir(siteId), "serverscripts", script.File))),
}));
specLines.push(`### ServerScripts`);
specLines.push(formatMarkdownTable(["Id", "Title", "ファイル", "要約"], serverRows));
}
}
const otherSettings = { ...settings };
["Permissions", "GridColumns", "EditorColumnHash", "Sections", "TitleColumns", "TitleSeparator", "Columns", "PermissionForCreating", "PermissionForUpdating", "Aggregations", "Links", "Processes", "Scripts", "Styles", "ServerScripts"].forEach((k) => delete otherSettings[k]);
const otherEntries = Object.entries(otherSettings).filter(([key, value]) => value !== null && value !== undefined && value !== "" && !(Array.isArray(value) && value.length === 0) && !(typeof value === "object" && Object.keys(value).length === 0));
if (otherEntries.length > 0) {
specLines.push(`## 11. その他設定`);
specLines.push(`| 設定キー | 値 |\n| --- | --- |`);
otherEntries.forEach(([key, value]) => {
specLines.push(`| ${key} | ${formatValue(value)} |`);
});
}
const alertRows = [];
if (settings?.Scripts?.some((script) => /API_KEY|SECRET|Bearer\s+|token|webhook|password/i.test(script.Body || ""))) {
alertRows.push("抽出されたスクリプト内に API キーやシークレット、Webhook URL、Bearerトークンなどの機密情報が含まれている可能性があります。内容を目視で確認してください。");
}
if (alertRows.length > 0) {
specLines.push(`## 気になる点(レビュー観点)`);
alertRows.forEach((line) => specLines.push(`- ${line}`));
}
return specLines.join("\n\n");
}
function generateOverviewMarkdown(siteId, data, settings, manifest) {
const title = data.Title || `Site ${siteId}`;
const sanitizedTitle = sanitizeName(title);
const overviewLines = [];
const columns = getArray(settings?.Columns || []).filter((col) => col.LabelText || col.DisplayName || col.Title);
const processes = manifest ? readJson(path.join(findExtractionDir(siteId), "processes.json")) : settings?.Processes || [];
const links = getArray(settings?.Links || []);
const guideText = [data.GridGuide, data.EditorGuide, data.CalendarGuide, data.GanttGuide, data.CrosstabGuide, data.TimeSeriesGuide, data.BurnDownGuide, data.AnalyGuide, data.KambanGuide].filter(Boolean).join("\n");
overviewLines.push(`# 概要書:${title}`);
overviewLines.push(`- この資料は業務担当者向けに、システム上の構成をもとに「何を管理し、どのような流れで使うか」を整理したものです。`);
overviewLines.push(`- 技術仕様の詳細は [./site-${siteId}_${sanitizedTitle}_spec.md](./site-${siteId}_${sanitizedTitle}_spec.md) を参照してください。\n`);
overviewLines.push(`## 1. これはどんな画面か`);
overviewLines.push(`このサイトは「${title}」を管理するための業務画面です。${data.ReferenceType || "Results"}として扱われ、登録・照会・更新の基本的な業務フローに沿って利用されます。`);
if (guideText) {
overviewLines.push(`- 画面上の案内では、${guideText.replace(/\n/g, " ").slice(0, 240)}${guideText.length > 240 ? "..." : ""}`);
}
if (columns.length > 0) {
overviewLines.push(`## 2. 主に入力・参照する情報`);
const importantFields = columns.filter((col) => {
const name = String(col.ColumnName || col.Name || "");
const label = String(col.LabelText || col.DisplayName || col.Title || "");
return /Status|ClassA|ClassB|ClassC|ClassD|ClassE|ClassF|ClassG|ClassH|ClassI|ClassJ|ClassK|ClassM|ClassN|ClassO|ClassZ|Date|Description|Attachments|Title/i.test(name + label);
});
const sourceFields = importantFields.length > 0 ? importantFields : columns;
const items = sourceFields.slice(0, 12).map((col) => ({
Field: formatValue(col.LabelText || col.DisplayName || col.Title || col.ColumnName || col.Name),
Purpose: inferOverviewPurpose(col),
}));
overviewLines.push(formatMarkdownTable(["項目", "用途"], items));
}
if (processes.length > 0) {
overviewLines.push(`## 3. 業務でよく行う操作`);
const flowRows = processes.slice(0, 12).map((proc) => ({
Operation: formatValue(proc.Name || proc.DisplayName || "プロセス"),
Change: formatValue(proc.CurrentStatus || "") + (proc.ChangedStatus ? `${proc.ChangedStatus}` : ""),
Description: inferProcessMeaning(proc),
}));
overviewLines.push(formatMarkdownTable(["操作", "状態の変化", "説明"], flowRows));
}
if (links.length > 0) {
overviewLines.push(`## 4. 他の管理表とのつながり`);
links.forEach((link) => {
const targetSite = formatValue(link.SiteId || link.TargetSiteId || link.ReferenceSiteId || "関連サイト");
const desc = inferLinkMeaning(link);
overviewLines.push(`- ${targetSite} の情報を参照・反映する連携です。${desc}`);
});
}
overviewLines.push(`## 5. 利用時のポイント`);
overviewLines.push(`- まずは画面上の入力項目を確認して、必要な情報を登録します。`);
overviewLines.push(`- 進捗や状態はステータス項目で管理し、必要に応じて他サイトの情報を参照します。`);
overviewLines.push(`- 重要な更新は履歴や添付情報と合わせて確認し、引き継ぎや問い合わせに備えます。`);
return { markdown: overviewLines.join("\n\n"), sanitizedTitle };
}
function inferOverviewPurpose(column) {
const name = String(column.ColumnName || column.Name || "");
const label = String(column.LabelText || column.DisplayName || column.Title || "");
const text = `${name}${label}`;
if (/Status/i.test(text)) return "登録状況や進捗を管理する項目です。";
if (/ClassA|管理番号|管理/i.test(text)) return "案件や物件を一意に識別するための項目です。";
if (/ClassB|登録者|担当|担当者/i.test(text)) return "誰が登録・担当しているかを管理する項目です。";
if (/Date|日時|日付/i.test(text)) return "予定や実施日などの時点を管理する項目です。";
if (/Description|備考|内容|コメント/i.test(text)) return "補足情報や業務メモを入力する項目です。";
if (/Attachments|添付/i.test(text)) return "関連書類や証跡を添付するための項目です。";
if (/Title/i.test(text)) return "件名や対象名を入力する項目です。";
if (/Class[0-9]{3}|Class[0-9]{2,3}/i.test(text)) return "業務上の分類コードを入力する項目です。";
return "業務上の情報を入力・保持するための項目です。";
}
function inferProcessMeaning(proc) {
const name = String(proc.Name || proc.DisplayName || "");
const current = String(proc.CurrentStatus || "");
const changed = String(proc.ChangedStatus || "");
const text = `${name}${current}${changed}`;
if (/承認|Approve|approval/i.test(text)) return "承認・確認の流れを進める操作です。";
if (/保留|Pending|hold/i.test(text)) return "一時停止や後続作業への待ち状態を作る操作です。";
if (/取消|Cancel|cancel/i.test(text)) return "登録内容を取り消す操作です。";
if (/終了|End|complete|完了/i.test(text)) return "業務を完了状態に遷移させる操作です。";
if (/登録|Create|new/i.test(text)) return "新規登録の初期状態を作る操作です。";
if (/チェック|Check/i.test(text)) return "入力内容や整合性を確認する操作です。";
return "業務状態を遷移させるための操作です。";
}
function inferLinkMeaning(link) {
const from = String(link.From || link.FromColumn || link.FromField || "");
const to = String(link.To || link.ToColumn || link.ToField || "");
if (from && to) return `${from}${to} の対応を取り、関連情報として参照します。`;
return "関連するマスタや別サイトの情報を参照する連携です。";
}
function resolveRequestedSiteIds(config) {
const cliEntries = process.argv.slice(2);
if (cliEntries.length > 0) {
return cliEntries.flatMap((entry) => parseSiteIds(entry));
}
return parseSiteIds(config.SiteId);
}
function generateAllDocs() {
if (!fs.existsSync(CONFIG_PATH)) {
console.error(`[エラー] 設定ファイルが見つかりません: ${CONFIG_PATH}`);
process.exit(1);
}
const config = readJson(CONFIG_PATH);
const siteIds = resolveRequestedSiteIds(config);
if (siteIds.length === 0) {
console.error("[エラー] config.json の SiteId から有効なサイトIDを取得できませんでした。");
process.exit(1);
}
ensureDir(CONFIGS_DIR);
ensureDir(DOCS_DIR);
console.log("[INFO] まず既存サイト情報を取得・抽出します...");
try {
child_process.execFileSync("node", [path.join(ROOT, "get-site-config.js")], {
stdio: "inherit",
cwd: ROOT,
});
} catch (err) {
console.error("[エラー] get-site-config.js の実行に失敗しました。取得処理を確認してください。");
process.exit(1);
}
for (const siteId of siteIds) {
console.log(`\n==== ドキュメント生成: SiteId=${siteId} ==== `);
const siteJsonPath = getSiteJsonPath(siteId);
if (!siteJsonPath) {
console.error(`[警告] site-${siteId}_latest.json が見つかりません。スキップします。`);
continue;
}
const json = readJson(siteJsonPath);
let data;
try {
data = resolveSiteData(json);
} catch (err) {
console.error(`[エラー] ${siteJsonPath} のデータ構造を解釈できませんでした: ${err.message}`);
continue;
}
const settings = data.SiteSettings || {};
const manifest = loadManifest(siteId);
const specMarkdown = generateSpecMarkdown(siteId, data, settings, manifest, siteJsonPath);
const titleToken = sanitizeName(data.Title || `site-${siteId}`);
const specPath = path.join(DOCS_DIR, `site-${siteId}_${titleToken}_spec.md`);
const legacySpecPath = path.join(CONFIGS_DIR, `site-${siteId}_spec.md`);
fs.writeFileSync(specPath, specMarkdown, "utf-8");
fs.writeFileSync(legacySpecPath, specMarkdown, "utf-8");
console.log(`[OK] 仕様書生成: ${specPath}`);
const { markdown: overviewMarkdown, sanitizedTitle } = generateOverviewMarkdown(siteId, data, settings, manifest);
const overviewName = `site-${siteId}_${sanitizedTitle}_overview.md`;
const overviewPath = path.join(DOCS_DIR, overviewName);
fs.writeFileSync(overviewPath, overviewMarkdown, "utf-8");
console.log(`[OK] 概要書生成: ${overviewPath}`);
try {
child_process.execFileSync("node", [path.join(ROOT, "md-to-pdf.js"), overviewPath], {
stdio: "inherit",
cwd: ROOT,
});
console.log(`[OK] PDF生成完了: ${overviewPath.replace(/\.md$/i, ".pdf")}`);
} catch (err) {
console.error(`[エラー] md-to-pdf.js による PDF 生成に失敗しました: ${overviewPath}`);
console.error(err.message || err);
}
}
}
if (require.main === module) {
generateAllDocs();
}