/** * generate-process-flowchart.js * ------------------------------------------------------------ * configs/site-{SiteId}_{サイト名}/processes.json と、サイトの生JSON * (Status列ChoicesText)から、Mermaid横向きフローチャート(Status遷移図)を * 生成し、標準出力へ表示します。 * ClaudePleasanter直下に置く共通実装。全プロジェクトの薄いラッパーから require される。 * * このスクリプトはファイル書き込み・API送信を一切行いません(標準出力のみ)。 * 出力された```mermaid```ブロックは、仕様書「8. プロセス設定」節へ手動で組み込んでください。 * * 使い方: * node generate-process-flowchart.js site-335411 * (フォルダ名は "site-{SiteId}_{サイト名}" 形式だが、"site-{SiteId}" だけの * 前方一致指定でも configs/ 配下から自動的に該当フォルダを探して解決する) * ------------------------------------------------------------ */ const fs = require("fs"); const path = require("path"); const { findLatestJsonPath } = require("./site-paths"); const { resolveProjectRoot } = require("./resolve-project"); function getLayout() { const v = process.env.PLEASANTER_LAYOUT; return v === "flat" || v === "separate" ? v : "hierarchical"; } function readJson(p) { return JSON.parse(fs.readFileSync(p, "utf-8")); } // フォルダ名は "site-{SiteId}_{サイト名}" 形式。"site-{SiteId}" のみの指定でも // configs/ 配下から前方一致で該当フォルダを探して解決する。 // (build-desired-config.js の resolveSiteDir と同一ロジック) function resolveSiteDir(baseDir, arg, env) { const configsDir = path.join(baseDir, "configs", env); const direct = path.isAbsolute(arg) ? arg : path.join(configsDir, path.basename(arg)); const baseName = path.basename(arg); const candidates = fs.existsSync(configsDir) ? fs.readdirSync(configsDir).filter((name) => { const full = path.join(configsDir, name); return ( fs.statSync(full).isDirectory() && (name === baseName || name.startsWith(`${baseName}_`)) ); }) : []; const suffixed = candidates.filter((name) => name !== baseName); if (suffixed.length === 1) return path.join(configsDir, suffixed[0]); if (suffixed.length > 1) { console.error(`[エラー] "${baseName}" に一致するフォルダが複数見つかりました: ${suffixed.join(", ")}`); console.error("フォルダ名をフルで指定してください。"); process.exit(1); } if (candidates.includes(baseName)) return path.join(configsDir, baseName); if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) return direct; console.error(`[エラー] フォルダが見つかりません: ${direct}`); console.error("先に node extract-site-config.js(またはnode get-site-config.js)を実行してください。"); process.exit(1); } // "値,ラベル,短縮ラベル,色" 形式(改行区切り)のChoicesTextを 値→ラベル のMapへ変換する function parseChoicesText(choicesText) { const map = new Map(); if (!choicesText) return map; const lines = choicesText.split("\n").filter((line) => line.trim().length > 0); for (const line of lines) { const parts = line.split(","); const value = (parts[0] || "").trim(); if (!value) continue; const label = (parts[1] || value).trim(); map.set(value, label); } return map; } // エッジのラベル文字列を組み立てる(プロセス名 + Dept注記 + 📧マーク) function formatEdgeLabel(process) { let label = process.DisplayName || process.Name || `Process${process.Id}`; if (Array.isArray(process.Depts) && process.Depts.length > 0) { label += ` (Dept${process.Depts.join(",")})`; } if (Array.isArray(process.Notifications) && process.Notifications.length > 0) { label = `📧${label}`; } return label; } // View.ColumnFilterHash.Status(JSON文字列化された配列)を文字列配列として取り出す。無ければnull。 function getFilterStatusList(process) { const raw = process?.View?.ColumnFilterHash?.Status; if (!raw) return null; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.map(String) : null; } catch (e) { return null; } } // processes配列から、Mermaidのエッジ文字列配列と、登場したStatusノード集合、 // "any"仮想ノードが必要かどうかを組み立てる。 function buildEdges(processes) { const edgeLines = []; const statusValues = new Set(); let needsAnyNode = false; for (const process of processes) { const current = String(process.CurrentStatus); const changed = String(process.ChangedStatus); const label = formatEdgeLabel(process); const filterList = getFilterStatusList(process); if (current !== "-1" && changed !== "-1") { statusValues.add(current); statusValues.add(changed); edgeLines.push(` ${current} -->|"${label}"| ${changed}`); } else if (current === "-1" && changed !== "-1") { statusValues.add(changed); if (filterList && filterList.length > 0) { for (const from of filterList) { statusValues.add(from); edgeLines.push(` ${from} -->|"${label}"| ${changed}`); } } else { needsAnyNode = true; edgeLines.push(` any -->|"${label}"| ${changed}`); } } else if (current !== "-1" && changed === "-1") { statusValues.add(current); edgeLines.push(` ${current} -->|"${label}"| ${current}`); } else { if (filterList && filterList.length > 0) { for (const target of filterList) { statusValues.add(target); edgeLines.push(` ${target} -->|"${label}"| ${target}`); } } else { needsAnyNode = true; edgeLines.push(` any -->|"${label}"| any`); } } } return { edgeLines, statusValues, needsAnyNode }; } // サイトの生JSON(getsiteダンプ)からStatus列のChoicesTextを取り出し、 // 値→ラベル のMapを返す。見つからない場合は空Mapを返し警告を出す。 function loadStatusLabels(baseDir, siteId, layout, env) { const configsDir = path.join(baseDir, "configs", env); const latestPath = findLatestJsonPath(baseDir, configsDir, siteId, layout, env); if (!latestPath) { console.error(`[警告] site-${siteId}_latest.json が見つかりません。ノードは値のみで表示します。`); return new Map(); } const json = readJson(latestPath); const columns = json?.Response?.Data?.SiteSettings?.Columns || []; const statusColumn = columns.find((col) => col.ColumnName === "Status"); if (!statusColumn || !statusColumn.ChoicesText) { console.error("[警告] Status列のChoicesTextが見つかりません。ノードは値のみで表示します。"); return new Map(); } return parseChoicesText(statusColumn.ChoicesText); } // Mermaid本文(```mermaid〜```込み)を組み立てる function buildMermaid(processes, statusLabels) { const { edgeLines, statusValues, needsAnyNode } = buildEdges(processes); const nodeLines = Array.from(statusValues).map((value) => { const label = statusLabels.get(value) || value; return ` ${value}((${label}
${value}))`; }); if (needsAnyNode) { nodeLines.push(" any((任意))"); } const lines = ["```mermaid", "flowchart LR", ...nodeLines, "", ...edgeLines, "```"]; return lines.join("\n"); } function main() { const { baseDir, env, argv } = resolveProjectRoot(); const layout = getLayout(); const arg = argv[0]; if (!arg) { console.error("[エラー] 対象フォルダを指定してください(例: node generate-process-flowchart.js site-335411 --env=production)"); process.exit(1); } const siteDir = resolveSiteDir(baseDir, arg, env); const processesPath = path.join(siteDir, "processes.json"); if (!fs.existsSync(processesPath)) { console.error(`[エラー] processes.jsonが見つかりません: ${processesPath}`); console.error("先に node get-site-config.js を実行してください。"); process.exit(1); } const processes = readJson(processesPath); const siteIdMatch = path.basename(siteDir).match(/^site-(\d+)/); const siteId = siteIdMatch ? siteIdMatch[1] : null; const statusLabels = siteId ? loadStatusLabels(baseDir, siteId, layout, env) : new Map(); const mermaid = buildMermaid(processes, statusLabels); console.log(mermaid); } if (require.main === module) { main(); } module.exports = { resolveSiteDir, parseChoicesText, formatEdgeLabel, getFilterStatusList, buildEdges, loadStatusLabels, buildMermaid, };