feat: add flow generation script and example flow documentation

This commit is contained in:
Marco Pedone 2026-03-27 16:53:47 +01:00
parent ccc27ddcb5
commit 319229830e
4 changed files with 210 additions and 3 deletions

View file

@ -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"]
```

View file

@ -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<string>();
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();

View file

@ -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"
}
}
}

View file

@ -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