@mmerterden/multi-agent-pipeline 12.1.1 → 12.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ // validate-analysis-doc.mjs - deterministic validator for an EMITTED
3
+ // /multi-agent:analysis document (analysis/<feature>-<platform>.md).
4
+ //
5
+ // The analysis SKILL states many "fails the dispatch gate" invariants as prose
6
+ // the model is asked to self-enforce. This turns the high-value, mechanically
7
+ // checkable ones into a real gate so a malformed spec is caught before it is
8
+ // handed to a human or to /multi-agent:dev.
9
+ //
10
+ // Zero deps. Reads one markdown file (path arg or STDIN).
11
+ //
12
+ // Checks (ERROR = blocking exit 1; WARN = advisory, still exit 0 unless --strict):
13
+ // - Front-matter block with required keys (feature, platform, language, mode,
14
+ // template_version); mode in {full, lite}; platform in the known set.
15
+ // - Never-omitted sections present by title keyword (Locked 2): Summary,
16
+ // Goals, User Stories, API Contracts, Architecture, Files to Add, Risks,
17
+ // References.
18
+ // - Humanizer punctuation policy (Locked 7): no em-dash / en-dash / ellipsis /
19
+ // section-sign / curly quotes anywhere in the body.
20
+ // - Traceability (Full mode only, Locked 31): every BR-<slug>-NN id defined in
21
+ // the Business Rules area is referenced again later (test / acceptance /
22
+ // analytics). A define-only rule is a WARN (heuristic).
23
+ // - No whole-section "TBD" / "Not applicable" placeholder bodies (Locked 2);
24
+ // the inline "[label TBD - see Open Questions]" marker is allowed.
25
+ //
26
+ // Usage:
27
+ // node validate-analysis-doc.mjs analysis/UserProfile-ios.md
28
+ // cat doc.md | node validate-analysis-doc.mjs -
29
+ // node validate-analysis-doc.mjs doc.md --strict # WARN also fails
30
+ //
31
+ // Exit: 0 valid, 1 invalid (or WARN under --strict), 64 usage error.
32
+
33
+ import { readFileSync } from "node:fs";
34
+
35
+ const KNOWN_PLATFORMS = new Set(["ios", "android", "backend", "frontend"]);
36
+ const REQUIRED_FM = ["feature", "platform", "language", "mode", "template_version"];
37
+
38
+ // Never-omitted sections (Locked 2), matched by bilingual title keyword so the
39
+ // re-flowed section numbering does not matter.
40
+ const REQUIRED_SECTIONS = [
41
+ { key: "summary", any: ["Summary", "Özet", "Ozet"] },
42
+ { key: "goals", any: ["Goals", "Hedefler"] },
43
+ { key: "user stories", any: ["User Stories", "Kullanıcı Hikayeleri", "Kullanici Hikayeleri"] },
44
+ { key: "api contracts", any: ["API Contracts", "API Kontratları", "API Kontratlari"] },
45
+ { key: "architecture", any: ["Architecture", "Mimari"] },
46
+ { key: "files to add", any: ["Files to Add", "Eklenecek Dosyalar"] },
47
+ { key: "risks", any: ["Risks", "Riskler"] },
48
+ { key: "references", any: ["References", "Referanslar"] },
49
+ ];
50
+
51
+ // Humanizer punctuation policy (Locked 7) - banned codepoints.
52
+ const BANNED_PUNCT = [
53
+ { ch: "—", name: "em-dash" },
54
+ { ch: "–", name: "en-dash" },
55
+ { ch: "…", name: "ellipsis" },
56
+ { ch: "§", name: "section-sign" },
57
+ { ch: "“", name: "curly-double-open" },
58
+ { ch: "”", name: "curly-double-close" },
59
+ { ch: "‘", name: "curly-single-open" },
60
+ { ch: "’", name: "curly-single-close" },
61
+ ];
62
+
63
+ function readInput() {
64
+ const args = process.argv.slice(2).filter((a) => a !== "--strict");
65
+ const arg = args[0];
66
+ if (!arg) {
67
+ console.error("usage: validate-analysis-doc.mjs <path|-> [--strict]");
68
+ process.exit(64);
69
+ }
70
+ if (arg === "-") return readFileSync(0, "utf-8");
71
+ return readFileSync(arg, "utf-8");
72
+ }
73
+
74
+ function parseFrontMatter(text) {
75
+ const lines = text.split("\n");
76
+ if (lines[0].trim() !== "---") return null;
77
+ const end = lines.indexOf("---", 1);
78
+ if (end < 0) return null;
79
+ const fm = {};
80
+ for (let i = 1; i < end; i++) {
81
+ const m = lines[i].match(/^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/);
82
+ if (m) fm[m[1]] = m[2].trim();
83
+ }
84
+ return { fm, bodyStart: end + 1 };
85
+ }
86
+
87
+ function main() {
88
+ const text = readInput();
89
+ const strict = process.argv.includes("--strict");
90
+ const errors = [];
91
+ const warns = [];
92
+
93
+ // 1. Front-matter
94
+ const parsed = parseFrontMatter(text);
95
+ if (!parsed) {
96
+ errors.push("missing YAML front-matter block (--- ... ---) at the top");
97
+ } else {
98
+ for (const k of REQUIRED_FM) {
99
+ if (!parsed.fm[k]) errors.push(`front-matter missing required key: ${k}`);
100
+ }
101
+ const mode = parsed.fm.mode;
102
+ if (mode && mode !== "full" && mode !== "lite") {
103
+ errors.push(`front-matter mode must be full|lite, got: ${mode}`);
104
+ }
105
+ const plat = parsed.fm.platform;
106
+ if (plat && !KNOWN_PLATFORMS.has(plat)) {
107
+ errors.push(`front-matter platform not in known set: ${plat}`);
108
+ }
109
+ }
110
+ const mode = parsed?.fm?.mode || "full";
111
+
112
+ // 2. Never-omitted sections (by heading keyword)
113
+ const headings = text
114
+ .split("\n")
115
+ .filter((l) => /^#{1,3}\s/.test(l))
116
+ .map((l) => l.replace(/^#{1,3}\s/, "").trim());
117
+ const headingBlob = headings.join("\n");
118
+ for (const sec of REQUIRED_SECTIONS) {
119
+ if (!sec.any.some((kw) => headingBlob.includes(kw))) {
120
+ errors.push(`missing required section (Locked 2): ${sec.key}`);
121
+ }
122
+ }
123
+
124
+ // 2b. Opt-in coverage sections must be present when the front-matter says so.
125
+ const uiTests = String(parsed?.fm?.ui_tests || "false").toLowerCase() === "true";
126
+ const a11yDepth = String(parsed?.fm?.a11y_depth || "basic").toLowerCase();
127
+ const headingBlobLower = headingBlob.toLowerCase();
128
+ if (uiTests) {
129
+ const hasUi =
130
+ headingBlob.includes("15.6") ||
131
+ headingBlobLower.includes("ui test") ||
132
+ headingBlobLower.includes("ui-test");
133
+ if (!hasUi) {
134
+ errors.push("front-matter ui_tests: true but no Section 15.6 UI test scenarios found (Locked 31)");
135
+ }
136
+ }
137
+ if (a11yDepth === "full") {
138
+ const hasWalkthrough =
139
+ headingBlob.includes("16.2") ||
140
+ headingBlobLower.includes("walkthrough") ||
141
+ headingBlobLower.includes("gezinme");
142
+ if (!hasWalkthrough) {
143
+ errors.push("front-matter a11y_depth: full but no Section 16.2 screen-reader walkthrough found");
144
+ }
145
+ }
146
+
147
+ // 3. Humanizer punctuation
148
+ const bodyLines = text.split("\n");
149
+ for (let i = 0; i < bodyLines.length; i++) {
150
+ for (const b of BANNED_PUNCT) {
151
+ if (bodyLines[i].includes(b.ch)) {
152
+ errors.push(`banned punctuation ${b.name} at line ${i + 1} (Locked 7 humanizer policy)`);
153
+ }
154
+ }
155
+ }
156
+
157
+ // 4. Placeholder whole-section bodies (Locked 2)
158
+ if (/^\s*(TBD|Not applicable|N\/A)\s*$/im.test(text)) {
159
+ warns.push('a line is a bare "TBD"/"Not applicable"/"N/A" body; omit the section instead (Locked 2)');
160
+ }
161
+
162
+ // 5. Traceability (Full mode only, Locked 31)
163
+ if (mode === "full") {
164
+ const idRe = /BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/gi;
165
+ const all = text.match(idRe) || [];
166
+ const counts = new Map();
167
+ for (const id of all) {
168
+ const key = id.toUpperCase();
169
+ counts.set(key, (counts.get(key) || 0) + 1);
170
+ }
171
+ if (counts.size === 0) {
172
+ warns.push("Full mode but no BR-<slug>-NN business-rule ids found (Section 4.4 / Locked 31)");
173
+ }
174
+ for (const [id, n] of counts) {
175
+ if (n < 2) {
176
+ warns.push(`business rule ${id} is defined but never referenced downstream (test/acceptance/analytics) - Locked 31 traceability`);
177
+ }
178
+ }
179
+ }
180
+
181
+ // Report
182
+ for (const w of warns) console.error(`WARN: ${w}`);
183
+ for (const e of errors) console.error(`ERROR: ${e}`);
184
+ if (errors.length > 0) {
185
+ console.error(`validate-analysis-doc: FAIL (${errors.length} error(s), ${warns.length} warning(s))`);
186
+ process.exit(1);
187
+ }
188
+ if (strict && warns.length > 0) {
189
+ console.error(`validate-analysis-doc: FAIL under --strict (${warns.length} warning(s))`);
190
+ process.exit(1);
191
+ }
192
+ console.log(`validate-analysis-doc: OK (mode=${mode}, ${warns.length} warning(s))`);
193
+ process.exit(0);
194
+ }
195
+
196
+ main();