From 319229830ed8b923dfc38222caac67318d36be65 Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Fri, 27 Mar 2026 16:53:47 +0100 Subject: [PATCH] feat: add flow generation script and example flow documentation --- apps/infoalloggi/docs/flows/example.md | 37 ++++++ apps/infoalloggi/flow-gen.ts | 168 +++++++++++++++++++++++++ apps/infoalloggi/package.json | 5 +- apps/infoalloggi/tsconfig.json | 3 +- 4 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 apps/infoalloggi/docs/flows/example.md create mode 100644 apps/infoalloggi/flow-gen.ts diff --git a/apps/infoalloggi/docs/flows/example.md b/apps/infoalloggi/docs/flows/example.md new file mode 100644 index 0000000..b706f3e --- /dev/null +++ b/apps/infoalloggi/docs/flows/example.md @@ -0,0 +1,37 @@ +# Flow Example + +```js +// FLOWS: example | 1 | Root Node + +// FLOWS: example | 2 | Test + +// FLOWS: example | 3 | Test2 + +// FLOWS: example | 3.1 | TestBranch + +// FLOWS: example | 3.2 | SecondBranch + +// FLOWS: example | 3.1.1 | TestBranchSub + +// FLOWS: example | 3.1.2 | TestBranchBranchSub + +// FLOWS: example | 3.2.1 | SecondBranchSub + +// FLOWS: example | 3.3 | MergeBack +``` + +```mermaid + +graph TD + step_1["Root Node"] --> step_2["Test"] + step_2["Test"] --> step_3["Test2"] + step_3["Test2"] --> step_3_1["TestBranch"] + step_3["Test2"] --> step_3_2["SecondBranch"] + step_3["Test2"] --> step_3_3["MergeBack"] + step_3_1["TestBranch"] --> step_3_1_1["TestBranchSub"] + step_3_1["TestBranch"] --> step_3_1_2["TestBranchBranchSub"] + step_3_1["TestBranch"] --> step_3_2["SecondBranch"] + step_3_1_1["TestBranchSub"] --> step_3_1_2["TestBranchBranchSub"] + step_3_2["SecondBranch"] --> step_3_2_1["SecondBranchSub"] + step_3_2["SecondBranch"] --> step_3_3["MergeBack"] +``` \ No newline at end of file diff --git a/apps/infoalloggi/flow-gen.ts b/apps/infoalloggi/flow-gen.ts new file mode 100644 index 0000000..b70bee4 --- /dev/null +++ b/apps/infoalloggi/flow-gen.ts @@ -0,0 +1,168 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; + +interface FlowStep { + id: string; + label: string; + major: number; + level: number; + file: string; // New: Track file path + line: number; // New: Track line number +} + +interface FlowMap { + [flowId: string]: FlowStep[]; +} + +/** CONFIGURATION **/ +const SRC_DIR = "./src"; +const OUTPUT_DIR = "./docs/flows"; +const FILE_EXTENSIONS = [".ts", ".js", ".tsx", ".jsx"]; + +// Updated Regex: Handles //FLOWS and // FLOWS +const FLOW_REGEX = /\/\/\s*FLOWS:\s*([^|]+)\s*\|\s*([\w.]+)\s*\|\s*(.*)/g; + +/** UTILS **/ +function getFiles(dir: string, allFiles: string[] = []): string[] { + if (!fs.existsSync(dir)) return allFiles; + const files = fs.readdirSync(dir); + for (const file of files) { + const name = path.join(dir, file); + if (fs.statSync(name).isDirectory()) getFiles(name, allFiles); + else if (FILE_EXTENSIONS.includes(path.extname(name))) allFiles.push(name); + } + return allFiles; +} + +function safeId(id: string): string { + // Mermaid IDs cannot contain dots + return `step_${id.replace(/\./g, "_")}`; +} + +/** CORE LOGIC **/ +function main() { + const flowData: FlowMap = {}; + const files = getFiles(SRC_DIR); + + console.log(`🔍 Scanning ${files.length} files for // FLOWS comments...`); + + // 1. Extraction + files.forEach((file) => { + const content = fs.readFileSync(file, "utf-8"); + const lines = content.split("\n"); + lines.forEach((lineText, index) => { + let match: RegExpExecArray | null; + FLOW_REGEX.lastIndex = 0; + match = FLOW_REGEX.exec(lineText); + while (match !== null) { + const [_, flowId, id, label] = match.map((s) => s.trim()); + if (!flowId || !id || !label) { + console.warn(`⚠️ Invalid FLOWS comment in ${file}: ${match[0]}`); + continue; + } + if (!flowData[flowId]) flowData[flowId] = []; + + flowData[flowId].push({ + id, + label, + major: parseInt(id.split(".")?.[0] || "0"), + level: id.split(".").length, + file: path.relative(process.cwd(), file), // Store relative path + line: index + 1, // Store line number (1-based) + }); + match = FLOW_REGEX.exec(lineText); + } + }); + }); + + if (Object.keys(flowData).length === 0) { + console.log( + "⚠️ No flow comments found. Try adding: // FLOWS: myflow | 1 | Start", + ); + return; + } + + // 2. Generate Output + if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + + for (const [flowId, steps] of Object.entries(flowData)) { + // Sort logically (1, 1.1, 1.2, 2) + const sorted = steps.sort((a, b) => + a.id.localeCompare(b.id, undefined, { numeric: true }), + ); + const mermaid = ["graph TD"]; + const connections = new Set(); + + sorted.forEach((current) => { + const nextSteps = sorted.filter((candidate) => { + const currentIdPrefix = `${current.id}.`; + + // 1. DESCEND: Link to immediate children (3 -> 3.1, 3 -> 3.2) + // Check if candidate starts with current ID and is exactly one level deeper + if ( + candidate.id.startsWith(currentIdPrefix) && + candidate.level === current.level + 1 + ) { + // Ensure it's a direct child (e.g., 3 -> 3.1, not 3 -> 3.1.1) + const remaining = candidate.id.replace(currentIdPrefix, ""); + return !remaining.includes("."); + } + + // 2. MERGE: Link leaf nodes to the next major integer (3.1.2 -> 4, 3.2.1 -> 4) + const isLeaf = !sorted.some( + (s) => s.id.startsWith(currentIdPrefix) && s.id !== current.id, + ); + if ( + isLeaf && + candidate.major === current.major + 1 && + candidate.level === 1 + ) { + return true; + } + + // 3. SEQUENCE (Optional): If you want 3.1.1 -> 3.1.2 + // If they share the exact same parent and are consecutive + const currentParent = current.id.includes(".") + ? current.id.substring(0, current.id.lastIndexOf(".")) + : null; + const candParent = candidate.id.includes(".") + ? candidate.id.substring(0, candidate.id.lastIndexOf(".")) + : null; + + if ( + currentParent !== null && + currentParent === candParent && + candidate.level === current.level + ) { + // Only link if you want branches to be sequential. + // In your case, you likely want 3.1.1 and 3.1.2 to both go to 4 independently. + // So we leave this FALSE to keep them as parallel paths. + return false; + } + + return false; + }); + + nextSteps.forEach((next) => { + const link = ` ${safeId(current.id)}["${current.label}"] --> ${safeId(next.id)}["${next.label}"]`; + connections.add(link); + }); + }); + + // Handle single-node flows (start node with no connections) + if (connections.size === 0 && sorted.length > 0 && sorted[0]) { + mermaid.push(` ${safeId(sorted[0].id)}["${sorted[0].label}"]`); + } else { + connections.forEach((c) => mermaid.push(c)); + } + + const outPath = path.join(OUTPUT_DIR, `${flowId}.md`); + + const header = "```mermaid\n\n"; + const footer = "\n```"; + fs.writeFileSync(outPath, header + mermaid.join("\n") + footer); + console.log(`✅ Generated: ${outPath}`); + } +} + +main(); diff --git a/apps/infoalloggi/package.json b/apps/infoalloggi/package.json index 81e88f2..cebc45e 100644 --- a/apps/infoalloggi/package.json +++ b/apps/infoalloggi/package.json @@ -17,7 +17,8 @@ "test": " npm run lint && npm run types ", "types": "tsc --noEmit", "analyze": "cross-env ANALYZE=true next build", - "vitest": "vitest run" + "vitest": "vitest run", + "flow": "ts-node flow-gen.ts" }, "version": "0.1.0", "dependencies": { @@ -138,4 +139,4 @@ "ct3aMetadata": { "initVersion": "7.9.0" } -} +} \ No newline at end of file diff --git a/apps/infoalloggi/tsconfig.json b/apps/infoalloggi/tsconfig.json index 696f517..03ed924 100644 --- a/apps/infoalloggi/tsconfig.json +++ b/apps/infoalloggi/tsconfig.json @@ -50,7 +50,8 @@ "public", "emails", ".next/types/**/*.ts", - "tests" + "tests", + "flow-gen.ts" ], "ts-node": { // these options are overrides used only by ts-node