@mmerterden/multi-agent-pipeline 18.0.0 → 19.1.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.
Files changed (234) hide show
  1. package/CHANGELOG.md +287 -0
  2. package/README.md +36 -20
  3. package/README.tr.md +14 -16
  4. package/docs/adr/0002-instruction-driven-flag.md +1 -0
  5. package/docs/adr/0005-lazy-phase-docs.md +11 -1
  6. package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +1 -0
  7. package/docs/adr/0010-own-code-graph.md +1 -0
  8. package/docs/adr/0014-six-phase-consolidation.md +134 -0
  9. package/docs/adr/README.md +2 -1
  10. package/docs/architecture.md +37 -38
  11. package/docs/best-practices.md +1 -1
  12. package/docs/ecosystem.md +46 -27
  13. package/docs/engineering.md +1 -1
  14. package/docs/facts.json +61 -0
  15. package/docs/features.md +55 -54
  16. package/docs/performance.md +5 -5
  17. package/docs/recovery-guide.md +17 -17
  18. package/docs/token-budget-history.md +3 -1
  19. package/index.js +2 -2
  20. package/install/_codex-agents.mjs +1 -1
  21. package/install/templates/claude-hooks.json +1 -1
  22. package/install/templates/codex-instructions.md +1 -1
  23. package/install/templates/copilot-instructions.md +28 -28
  24. package/manifest.json +234 -216
  25. package/package.json +2 -2
  26. package/pipeline/agents/dev-critic.md +7 -7
  27. package/pipeline/commands/figma-to-swiftui.md +1 -1
  28. package/pipeline/commands/multi-agent/SKILL.md +9 -9
  29. package/pipeline/commands/multi-agent/analysis/SKILL.md +15 -15
  30. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  31. package/pipeline/commands/multi-agent/autopilot/SKILL.md +7 -7
  32. package/pipeline/commands/multi-agent/channels/SKILL.md +15 -15
  33. package/pipeline/commands/multi-agent/diff-explain/SKILL.md +6 -6
  34. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
  35. package/pipeline/commands/multi-agent/graph/SKILL.md +1 -1
  36. package/pipeline/commands/multi-agent/help/SKILL.md +62 -62
  37. package/pipeline/commands/multi-agent/language/SKILL.md +2 -2
  38. package/pipeline/commands/multi-agent/local/SKILL.md +11 -11
  39. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +13 -13
  40. package/pipeline/commands/multi-agent/log/SKILL.md +2 -2
  41. package/pipeline/commands/multi-agent/manual-test/SKILL.md +9 -9
  42. package/pipeline/commands/multi-agent/model/SKILL.md +69 -0
  43. package/pipeline/commands/multi-agent/refactor/SKILL.md +3 -3
  44. package/pipeline/commands/multi-agent/resume/SKILL.md +4 -4
  45. package/pipeline/commands/multi-agent/resume-local/SKILL.md +19 -17
  46. package/pipeline/commands/multi-agent/review/SKILL.md +2 -2
  47. package/pipeline/commands/multi-agent/review-analysis/SKILL.md +1 -1
  48. package/pipeline/commands/multi-agent/route-off/SKILL.md +36 -0
  49. package/pipeline/commands/multi-agent/route-on/SKILL.md +74 -0
  50. package/pipeline/commands/multi-agent/route-status/SKILL.md +56 -0
  51. package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
  52. package/pipeline/commands/multi-agent/status/SKILL.md +5 -5
  53. package/pipeline/commands/multi-agent/steer/SKILL.md +2 -2
  54. package/pipeline/commands/multi-agent/sync/SKILL.md +12 -13
  55. package/pipeline/commands/multi-agent/test/SKILL.md +1 -1
  56. package/pipeline/lib/credential-inventory.sh +1 -1
  57. package/pipeline/lib/fetch-fortify.sh +1 -1
  58. package/pipeline/lib/model-dispatch.sh +140 -0
  59. package/pipeline/lib/model-rung.sh +142 -0
  60. package/pipeline/lib/outbound-gate.mjs +14 -0
  61. package/pipeline/lib/phase-schema.mjs +88 -0
  62. package/pipeline/lib/plan-todos.sh +5 -5
  63. package/pipeline/lib/route-state.sh +161 -0
  64. package/pipeline/lib/run-paths.sh +2 -2
  65. package/pipeline/multi-agent-refs/_account-picker.md +1 -1
  66. package/pipeline/multi-agent-refs/_dev-context.md +6 -6
  67. package/pipeline/multi-agent-refs/_input-parser.md +1 -1
  68. package/pipeline/multi-agent-refs/analysis/evidence.md +2 -11
  69. package/pipeline/multi-agent-refs/analysis/intake.md +7 -7
  70. package/pipeline/multi-agent-refs/analysis/locked.md +48 -22
  71. package/pipeline/multi-agent-refs/analysis/redesign.md +1 -1
  72. package/pipeline/multi-agent-refs/analysis/render.md +10 -10
  73. package/pipeline/multi-agent-refs/analysis/resolve.md +1 -1
  74. package/pipeline/multi-agent-refs/analysis/review.md +2 -2
  75. package/pipeline/multi-agent-refs/analysis/synthesis.md +13 -7
  76. package/pipeline/multi-agent-refs/analysis-template-corporate.md +9 -9
  77. package/pipeline/multi-agent-refs/analysis-template.md +19 -19
  78. package/pipeline/multi-agent-refs/android-guide.md +1 -1
  79. package/pipeline/multi-agent-refs/audit-guide.md +13 -13
  80. package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
  81. package/pipeline/multi-agent-refs/channels/jira.md +3 -3
  82. package/pipeline/multi-agent-refs/channels/pr.md +4 -4
  83. package/pipeline/multi-agent-refs/channels/wiki.md +1 -1
  84. package/pipeline/multi-agent-refs/component-dispatch.md +8 -8
  85. package/pipeline/multi-agent-refs/conventions-defaults.md +2 -2
  86. package/pipeline/multi-agent-refs/cross-cli-contract.md +31 -6
  87. package/pipeline/multi-agent-refs/features/analysis-jira.md +1 -1
  88. package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +4 -4
  89. package/pipeline/multi-agent-refs/features/code-graph.md +5 -5
  90. package/pipeline/multi-agent-refs/features/design-conformance.md +1 -1
  91. package/pipeline/multi-agent-refs/features/dev-critic.md +3 -3
  92. package/pipeline/multi-agent-refs/features/doctor.md +3 -3
  93. package/pipeline/multi-agent-refs/features/external-context-injection.md +3 -3
  94. package/pipeline/multi-agent-refs/features/maturity-followup.md +3 -3
  95. package/pipeline/multi-agent-refs/features/model-fallback.md +41 -5
  96. package/pipeline/multi-agent-refs/features/plan-todos.md +1 -1
  97. package/pipeline/multi-agent-refs/features/repo-map.md +1 -1
  98. package/pipeline/multi-agent-refs/features/review-delta.md +3 -3
  99. package/pipeline/multi-agent-refs/features/review-multi-repo.md +2 -2
  100. package/pipeline/multi-agent-refs/features/scope-check.md +4 -4
  101. package/pipeline/multi-agent-refs/features/skill-conformance.md +2 -2
  102. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +1 -1
  103. package/pipeline/multi-agent-refs/features/url-enrichment.md +1 -1
  104. package/pipeline/multi-agent-refs/features/verify-by-test.md +4 -4
  105. package/pipeline/multi-agent-refs/features/visual-evidence.md +19 -19
  106. package/pipeline/multi-agent-refs/features/worktree-finalize.md +6 -6
  107. package/pipeline/multi-agent-refs/issue-jira-triad.md +10 -10
  108. package/pipeline/multi-agent-refs/knowledge.md +11 -11
  109. package/pipeline/multi-agent-refs/multi-repo-integration-build.md +13 -13
  110. package/pipeline/multi-agent-refs/payload-contracts.md +8 -8
  111. package/pipeline/multi-agent-refs/phases/log-format.md +10 -10
  112. package/pipeline/multi-agent-refs/phases/modes.md +30 -30
  113. package/pipeline/multi-agent-refs/phases/operations.md +8 -8
  114. package/pipeline/multi-agent-refs/phases/phase-0-init.md +24 -24
  115. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +599 -0
  116. package/pipeline/multi-agent-refs/phases/{phase-3-dev.md → phase-2-dev.md} +129 -49
  117. package/pipeline/multi-agent-refs/phases/{phase-4-review.md → phase-3-review.md} +225 -107
  118. package/pipeline/multi-agent-refs/phases/{phase-6-commit.md → phase-4-commit.md} +23 -23
  119. package/pipeline/multi-agent-refs/phases/{phase-7-report.md → phase-5-report.md} +29 -29
  120. package/pipeline/multi-agent-refs/phases.md +44 -48
  121. package/pipeline/multi-agent-refs/picker-contract.md +1 -1
  122. package/pipeline/multi-agent-refs/progress-contract.md +6 -6
  123. package/pipeline/multi-agent-refs/readiness-review.md +1 -1
  124. package/pipeline/multi-agent-refs/rules.md +7 -7
  125. package/pipeline/multi-agent-refs/swiftui-guide.md +2 -2
  126. package/pipeline/multi-agent-refs/tracker-contract.md +31 -32
  127. package/pipeline/multi-agent-refs/wiki-capture.md +14 -14
  128. package/pipeline/preferences-template.json +9 -1
  129. package/pipeline/rules/figma-pipeline.md +8 -8
  130. package/pipeline/rules/outside-the-pipeline.md +1 -1
  131. package/pipeline/schemas/agent-state.schema.json +50 -50
  132. package/pipeline/schemas/analysis-output.schema.json +3 -3
  133. package/pipeline/schemas/analysis-spec.schema.json +2 -2
  134. package/pipeline/schemas/autopilot-config.schema.json +1 -1
  135. package/pipeline/schemas/code-graph.schema.json +1 -1
  136. package/pipeline/schemas/criteria-manifest.schema.json +1 -1
  137. package/pipeline/schemas/dev-critic-output.schema.json +1 -1
  138. package/pipeline/schemas/diff-risk.schema.json +1 -1
  139. package/pipeline/schemas/figma-project-config.schema.json +1 -1
  140. package/pipeline/schemas/migrations/prefs-2.4.0-to-2.5.0.mjs +2 -2
  141. package/pipeline/schemas/migrations/prefs-2.6.0-to-2.7.0.mjs +31 -0
  142. package/pipeline/schemas/migrations/state-2.1.0-to-2.2.0.mjs +129 -0
  143. package/pipeline/schemas/phases.json +105 -0
  144. package/pipeline/schemas/plan-todos.schema.json +5 -5
  145. package/pipeline/schemas/planning-output.schema.json +1 -1
  146. package/pipeline/schemas/prefs.schema.json +102 -58
  147. package/pipeline/schemas/reviewer-output.schema.json +3 -3
  148. package/pipeline/schemas/route-config.schema.json +74 -0
  149. package/pipeline/schemas/scope-check.schema.json +1 -1
  150. package/pipeline/schemas/secret-patterns.json +124 -0
  151. package/pipeline/schemas/test-gap.schema.json +1 -1
  152. package/pipeline/schemas/token-budget.json +12 -18
  153. package/pipeline/schemas/triage-output.schema.json +6 -6
  154. package/pipeline/scripts/README.md +3 -3
  155. package/pipeline/scripts/_code-graph.mjs +2 -2
  156. package/pipeline/scripts/_run-paths.mjs +2 -2
  157. package/pipeline/scripts/_smoke-root.sh +1 -1
  158. package/pipeline/scripts/aggregate-metrics.mjs +1 -1
  159. package/pipeline/scripts/build-references.mjs +2 -2
  160. package/pipeline/scripts/bulk-read.sh +10 -1
  161. package/pipeline/scripts/capture-flush.sh +8 -8
  162. package/pipeline/scripts/capture-resume.sh +3 -3
  163. package/pipeline/scripts/classify-plan-safety.mjs +1 -1
  164. package/pipeline/scripts/cost-table.json +8 -1
  165. package/pipeline/scripts/diff-explain.mjs +1 -1
  166. package/pipeline/scripts/doctor.mjs +3 -3
  167. package/pipeline/scripts/gc-abandoned.sh +3 -3
  168. package/pipeline/scripts/gc-tmp.sh +1 -1
  169. package/pipeline/scripts/gc-worktrees.sh +1 -1
  170. package/pipeline/scripts/gen-facts.mjs +280 -0
  171. package/pipeline/scripts/gen-mode-dispatch.mjs +32 -37
  172. package/pipeline/scripts/gen-ref-toc.mjs +1 -1
  173. package/pipeline/scripts/graph-report.mjs +1 -1
  174. package/pipeline/scripts/jira-attach.sh +1 -1
  175. package/pipeline/scripts/learn-from-transcripts.mjs +1 -1
  176. package/pipeline/scripts/learning-curve.mjs +2 -2
  177. package/pipeline/scripts/log-metric.sh +17 -4
  178. package/pipeline/scripts/memory-save.sh +1 -1
  179. package/pipeline/scripts/migrate-prefs.mjs +22 -5
  180. package/pipeline/scripts/phase-banner.sh +20 -20
  181. package/pipeline/scripts/phase-tracker.sh +12 -12
  182. package/pipeline/scripts/plan-coverage-gate.mjs +2 -2
  183. package/pipeline/scripts/pre-commit-check.sh +30 -1
  184. package/pipeline/scripts/render-agent-log-cost.sh +1 -1
  185. package/pipeline/scripts/render-work-summary.sh +3 -3
  186. package/pipeline/scripts/review-file-filter.mjs +1 -1
  187. package/pipeline/scripts/run-aggregator.mjs +13 -6
  188. package/pipeline/scripts/run-metrics.mjs +1 -1
  189. package/pipeline/scripts/runs-index.mjs +11 -1
  190. package/pipeline/scripts/scan-skills.sh +26 -0
  191. package/pipeline/scripts/smoke-cross-cli-behavior.sh +6 -6
  192. package/pipeline/scripts/smoke-schema-validation.sh +26 -7
  193. package/pipeline/scripts/token-budget-report.mjs +13 -2
  194. package/pipeline/scripts/triage-memory.mjs +2 -2
  195. package/pipeline/scripts/validate-analysis-doc.mjs +274 -43
  196. package/pipeline/scripts/validate-planning.mjs +1 -1
  197. package/pipeline/scripts/validate-reviewer.mjs +1 -1
  198. package/pipeline/scripts/validate-state.mjs +45 -5
  199. package/pipeline/scripts/validate-triage.mjs +3 -3
  200. package/pipeline/scripts/verify-citations.mjs +1 -1
  201. package/pipeline/scripts/worktree-finalize.sh +5 -5
  202. package/pipeline/scripts/write-state.mjs +32 -0
  203. package/pipeline/skills/.skill-manifest.json +38 -22
  204. package/pipeline/skills/.skills-index.json +49 -5
  205. package/pipeline/skills/shared/README.md +10 -6
  206. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +8 -8
  207. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +8 -8
  208. package/pipeline/skills/shared/core/multi-agent/SKILL.md +81 -82
  209. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +3 -3
  210. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +14 -14
  211. package/pipeline/skills/shared/core/multi-agent-diff-explain/SKILL.md +5 -5
  212. package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +1 -1
  213. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +25 -23
  214. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +2 -2
  215. package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +2 -2
  216. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +8 -8
  217. package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +6 -6
  218. package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +71 -0
  219. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +3 -3
  220. package/pipeline/skills/shared/core/multi-agent-resume/SKILL.md +1 -1
  221. package/pipeline/skills/shared/core/multi-agent-resume-local/SKILL.md +7 -7
  222. package/pipeline/skills/shared/core/multi-agent-route-off/SKILL.md +39 -0
  223. package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +76 -0
  224. package/pipeline/skills/shared/core/multi-agent-route-status/SKILL.md +59 -0
  225. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
  226. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +5 -5
  227. package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +2 -2
  228. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +6 -5
  229. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +1 -1
  230. package/pipeline/skills/shared/external/signal-community/SKILL.md +8 -1
  231. package/pipeline/skills/skills-index.md +8 -4
  232. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +0 -263
  233. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +0 -344
  234. package/pipeline/multi-agent-refs/phases/phase-5-test.md +0 -182
@@ -11,7 +11,7 @@
11
11
  //
12
12
  // Checks (ERROR = blocking exit 1; WARN = advisory, still exit 0 unless --strict):
13
13
  // - Front-matter block with required keys (feature, platform, language, mode,
14
- // template_version); mode in {full, lite}; platform in the known set.
14
+ // template_version); mode must be full (lite removed in v19.0.0); platform in the known set.
15
15
  // - Never-omitted sections present by title keyword (Locked 2): Summary,
16
16
  // Goals, User Stories, API Contracts, Architecture, Files to Add, Risks,
17
17
  // References.
@@ -27,7 +27,7 @@
27
27
  // - Manual scenarios (Section 15.7): every MT- row carries a BR- id.
28
28
  // - Variant coverage (Section 6.X): every axis row states the full axis and
29
29
  // the subset this screen uses; half a row cannot be audited.
30
- // - Traceability (Full mode only, Locked 31): every BR-<slug>-NN id defined in
30
+ // - Traceability (Full mode only, Locked 30): every BR-<slug>-NN id defined in
31
31
  // the Business Rules area is referenced again later (test / acceptance /
32
32
  // analytics). A define-only rule is a WARN (heuristic).
33
33
  // - No whole-section "TBD" / "Not applicable" placeholder bodies (Locked 2);
@@ -43,7 +43,7 @@
43
43
  import { readFileSync } from "node:fs";
44
44
  import { runMain } from "../lib/fatal.mjs";
45
45
 
46
- // A repo-less run (Locked 35) still splits by channel, derived from the evidence:
46
+ // A repo-less run (Locked 34) still splits by channel, derived from the evidence:
47
47
  // "mobile" and "web" are its platform values. "none" is the narrower case where the
48
48
  // evidence describes no interface at all. All three are real values, not missing ones.
49
49
  // "web" is the pre-"web" spelling, still accepted so older documents validate.
@@ -63,7 +63,7 @@ const REQUIRED_SECTIONS = [
63
63
  { key: "references", any: ["References", "Referanslar"] },
64
64
  ];
65
65
 
66
- // Corporate profile backbone (Locked 33). These render even with zero evidence,
66
+ // Corporate profile backbone (Locked 32). These render even with zero evidence,
67
67
  // carrying N/A or EKLENECEK, so their absence is a structural failure rather
68
68
  // than a legitimate omission.
69
69
  const CORPORATE_SECTIONS = [
@@ -245,16 +245,31 @@ function main() {
245
245
  for (const k of REQUIRED_FM) {
246
246
  if (!parsed.fm[k]) errors.push(`front-matter missing required key: ${k}`);
247
247
  }
248
+ // `mode` is accepted only as "full" now. Lite was removed in v19.0.0: it
249
+ // picked sections from a fixed list chosen by a three-signal score, which
250
+ // fought Locked 2 in both directions - a small feature with rich business
251
+ // rules lost Section 15 because it was not on the list, and a feature with
252
+ // no API contract kept Section 9 because it was. An older document carrying
253
+ // `mode: lite` is still readable; it is reported as a legacy value rather
254
+ // than rejected, because the document was valid when it was written.
248
255
  const mode = parsed.fm.mode;
249
- if (mode && mode !== "full" && mode !== "lite") {
250
- errors.push(`front-matter mode must be full|lite, got: ${mode}`);
256
+ if (mode === "lite") {
257
+ warns.push(
258
+ "front-matter says mode: lite - Lite was removed in v19.0.0 and this is a pre-v19 document. Sections are chosen by evidence now (Locked 2).",
259
+ );
260
+ } else if (mode && mode !== "full") {
261
+ errors.push(`front-matter mode must be full, got: ${mode}`);
251
262
  }
252
263
  const plat = parsed.fm.platform;
253
264
  if (plat && !KNOWN_PLATFORMS.has(plat)) {
254
265
  errors.push(`front-matter platform not in known set: ${plat}`);
255
266
  }
256
267
  }
257
- const mode = parsed?.fm?.mode || "full";
268
+ // A pre-v19 `lite` document is validated as Full: the checks it skipped were
269
+ // skipped because of a mode that no longer exists, and re-skipping them here
270
+ // would carry that exemption forward forever.
271
+ const mode = "full";
272
+ void parsed?.fm?.mode;
258
273
  const profile = (parsed?.fm?.profile || "global").toLowerCase();
259
274
  if (profile !== "global" && profile !== "corporate") {
260
275
  errors.push(`front-matter profile must be global|corporate, got: ${profile}`);
@@ -275,7 +290,7 @@ function main() {
275
290
  // The development layer is legitimately absent when no repo was selected,
276
291
  // so requiring it there would fail every stack-optional document.
277
292
  .filter((sec) => !(noPlatform && REPO_DEPENDENT_KEYS.has(sec.key)));
278
- const rule = profile === "corporate" ? "Locked 33" : "Locked 2";
293
+ const rule = profile === "corporate" ? "Locked 32" : "Locked 2";
279
294
  for (const sec of requiredSections) {
280
295
  if (!sec.any.some((kw) => headingBlob.includes(kw))) {
281
296
  errors.push(`missing required section (${rule}): ${sec.key}`);
@@ -301,7 +316,7 @@ function main() {
301
316
  //
302
317
  // It ran only in the corporate profile until now, which made the global one the
303
318
  // profile where an inline marker could point at nothing. Two documents already
304
- // told it to: Locked 36 says an unstamped gap fails the dispatch gate with no
319
+ // told it to: Locked 35 says an unstamped gap fails the dispatch gate with no
305
320
  // profile clause, and analysis/render.md routes an external-dependency gap to
306
321
  // `AS-NN` in Section 20 for every run. The check was the only side that
307
322
  // disagreed. Nothing changes for a document that carries no markers: every loop
@@ -326,13 +341,13 @@ function main() {
326
341
  // A gap that names no id cannot be traced to an owner, which is the whole
327
342
  // point of admitting it.
328
343
  if (!/\bAS-\d{2,3}\b/.test(lines[i])) {
329
- errors.push(`line ${i + 1}: EKLENECEK carries no AS-NN reference (Locked 33)`);
344
+ errors.push(`line ${i + 1}: EKLENECEK carries no AS-NN reference (Locked 32)`);
330
345
  }
331
346
  }
332
347
  for (const id of referencedIds) {
333
348
  if (!definedIds.has(id)) {
334
349
  errors.push(
335
- `${id} is referenced in the body but not defined in Risks and Open Questions (Locked 33)`,
350
+ `${id} is referenced in the body but not defined in Risks and Open Questions (Locked 32)`,
336
351
  );
337
352
  }
338
353
  }
@@ -343,7 +358,7 @@ function main() {
343
358
  const row = risksBlock.split("\n").find((l) => l.includes(id)) || "";
344
359
  if (!/(no body reference|govde referansi yok)/i.test(row)) {
345
360
  errors.push(
346
- `${id} is defined in Risks and Open Questions but never referenced in the body (Locked 33)`,
361
+ `${id} is defined in Risks and Open Questions but never referenced in the body (Locked 32)`,
347
362
  );
348
363
  }
349
364
  }
@@ -379,13 +394,13 @@ function main() {
379
394
  const owner = cells[oi] || "";
380
395
  if (!owner || /^(-|\?|TBD|EKLENECEK|N\/A)$/i.test(owner)) {
381
396
  const id = (row.match(/\bAS-\d{2,3}\b/) || ["a row"])[0];
382
- errors.push(`${id} names no owner in Risks and Open Questions (Locked 36)`);
397
+ errors.push(`${id} names no owner in Risks and Open Questions (Locked 35)`);
383
398
  }
384
399
  }
385
400
  } else if (rows.some((l) => /\bAS-\d{2,3}\b/.test(l))) {
386
401
  // Only when there is something to own. A Section 20 with no stamped row
387
402
  // has no owner to check, and warning there would fire on every document
388
- // that never needed the column - noise that Phase 4 runs under --strict.
403
+ // that never needed the column - noise that Phase 3 runs under --strict.
389
404
  warns.push(
390
405
  "Risks and Open Questions carries AS-NN rows but no owner column; ownership cannot be checked",
391
406
  );
@@ -424,7 +439,7 @@ function main() {
424
439
  }
425
440
  }
426
441
  for (const id of [...openMarkers].sort()) {
427
- errors.push(`status: final but ${id} is still open (Locked 36 closure gate)`);
442
+ errors.push(`status: final but ${id} is still open (Locked 35 closure gate)`);
428
443
  }
429
444
  const stillOpen =
430
445
  risksStatusIdx >= 0
@@ -474,7 +489,7 @@ function main() {
474
489
  if (mode === "full") {
475
490
  const matrixStart = lines.findIndex((l) => /^#{2,4}\s/.test(l) && MATRIX_HEADING.test(l));
476
491
  if (matrixStart === -1) {
477
- errors.push("corporate Full mode requires the requirement traceability matrix (Locked 33)");
492
+ errors.push("corporate Full mode requires the requirement traceability matrix (Locked 32)");
478
493
  } else {
479
494
  let matrixBlock = "";
480
495
  for (let i = matrixStart + 1; i < lines.length; i++) {
@@ -505,7 +520,7 @@ function main() {
505
520
  }
506
521
  mark("traceability-matrix");
507
522
  } else {
508
- mark("traceability-matrix", "lite mode");
523
+ mark("traceability-matrix");
509
524
  }
510
525
  } else {
511
526
  mark("payload-in-table-cell", "global profile");
@@ -562,7 +577,7 @@ function main() {
562
577
  headingBlobLower.includes("ui-test");
563
578
  if (!hasUi) {
564
579
  errors.push(
565
- "front-matter ui_tests: true but no Section 15.6 UI test scenarios found (Locked 31)",
580
+ "front-matter ui_tests: true but no Section 15.6 UI test scenarios found (Locked 30)",
566
581
  );
567
582
  }
568
583
  }
@@ -580,12 +595,12 @@ function main() {
580
595
 
581
596
  mark("optin-coverage-sections");
582
597
 
583
- // 2c. The Test Plan is Phase 3's RED input (phase-3-dev.md step 5b): the tests
598
+ // 2c. The Test Plan is Phase 3's RED input (phase-2-dev.md step 5b): the tests
584
599
  // written first come from it. Full mode without it hands dev an empty matrix.
585
600
  // It is Section 15 in the global profile and Section 19 in the corporate one;
586
601
  // the check matches on the title, so only the stack-optional case needs an
587
602
  // exemption - there the whole development layer is legitimately absent
588
- // (Locked 35) and there is no repo for a test to run against yet.
603
+ // (Locked 34) and there is no repo for a test to run against yet.
589
604
  if (mode === "full" && !noPlatform) {
590
605
  const hasTestPlan = ["Test Planı", "Test Plani", "Test Plan"].some((kw) =>
591
606
  headingBlob.includes(kw),
@@ -594,12 +609,12 @@ function main() {
594
609
  // The omission table allows exactly one case: a backend-only run with no
595
610
  // contract testing planned. The validator knows the platform, so that case
596
611
  // warns instead of blocking; every other platform hands dev an empty matrix.
597
- const msg = "no Section 15 Test Plan; Full mode dev reads it as the RED input (Locked 31)";
612
+ const msg = "no Section 15 Test Plan; dev reads it as the RED input (Locked 30)";
598
613
  if (parsed?.fm?.platform === "backend") {
599
614
  warns.push(`${msg} - allowed for backend only when no contract testing is planned`);
600
615
  } else {
601
616
  errors.push(
602
- `missing Section 15 Test Plan; Full mode dev reads it as the RED input (Locked 31)`,
617
+ `missing Section 15 Test Plan; Full mode dev reads it as the RED input (Locked 30)`,
603
618
  );
604
619
  }
605
620
  } else {
@@ -609,14 +624,13 @@ function main() {
609
624
  headingBlobLower.includes("unit test");
610
625
  if (!hasUnit && /BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/i.test(text)) {
611
626
  errors.push(
612
- "Section 15 has no unit-test sub-table while business rules are defined (Locked 31)",
627
+ "Section 15 has no unit-test sub-table while business rules are defined (Locked 30)",
613
628
  );
614
629
  }
615
630
  }
616
631
  }
617
632
 
618
- if (mode !== "full") mark("test-plan", "lite mode");
619
- else if (noPlatform) mark("test-plan", "no target platform");
633
+ if (noPlatform) mark("test-plan", "no target platform");
620
634
  else mark("test-plan");
621
635
 
622
636
  // 2d. Section 3 carries the diagrams. Omission is legitimate under the
@@ -726,7 +740,7 @@ function main() {
726
740
 
727
741
  mark("concept-table-evidence", conceptBody ? undefined : "no concept table");
728
742
 
729
- // 2g. Locked 31 main clause: a rule with no test scenario fails the gate.
743
+ // 2g. Locked 30 main clause: a rule with no test scenario fails the gate.
730
744
  // The pre-existing occurrence-count heuristic below stays as the looser net.
731
745
  if (mode === "full") {
732
746
  const idRx = /BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/gi;
@@ -741,15 +755,15 @@ function main() {
741
755
  const tested = new Set((testBody.join("\n").match(idRx) || []).map((x) => x.toUpperCase()));
742
756
  for (const id of defined) {
743
757
  if (!tested.has(id)) {
744
- errors.push(`business rule ${id} has no Section 15 test scenario (Locked 31)`);
758
+ errors.push(`business rule ${id} has no Section 15 test scenario (Locked 30)`);
745
759
  }
746
760
  }
747
761
  }
748
762
  }
749
763
 
750
- mark("rule-to-test", mode === "full" ? undefined : "lite mode");
764
+ mark("rule-to-test");
751
765
 
752
- // 2h. Redesign mode (Locked 37). Eight checks, live only when the front-matter
766
+ // 2h. Redesign mode (Locked 36). Eight checks, live only when the front-matter
753
767
  // opts in. The contract they enforce is analysis/redesign.md, which loads only
754
768
  // on a redesign run; what is here is the enforcement, not the explanation.
755
769
  const redesign = String(parsed?.fm?.redesign || "false").toLowerCase() === "true";
@@ -773,7 +787,7 @@ function main() {
773
787
  [epBody, "4.6 Endpoint Mapping"],
774
788
  [dlBody, "9.5 Difference List"],
775
789
  ]) {
776
- if (!body) errors.push(`redesign: true but Section ${name} is missing (Locked 37)`);
790
+ if (!body) errors.push(`redesign: true but Section ${name} is missing (Locked 36)`);
777
791
  }
778
792
 
779
793
  const STATUSES = ["Moved", "Partial", "Missing", "New", "Out of scope"];
@@ -790,11 +804,11 @@ function main() {
790
804
  // the row that was not marked.
791
805
  if (!/:\d+\b/.test(evidence) && !certainty.includes("uncertain")) {
792
806
  errors.push(
793
- `current-behaviour row ${id} cites no file:line and is not marked uncertain (Locked 37)`,
807
+ `current-behaviour row ${id} cites no file:line and is not marked uncertain (Locked 36)`,
794
808
  );
795
809
  }
796
810
  // 3. A rule that lives only in a shared module legitimately reads this way,
797
- // so it warns rather than blocks - and Phase 4 runs --strict, so it blocks
811
+ // so it warns rather than blocks - and Phase 3 runs --strict, so it blocks
798
812
  // there, which is where a human is already looking.
799
813
  if (/cross-cutting/i.test(evidence) && certainty.includes("confirmed")) {
800
814
  warns.push(
@@ -811,14 +825,14 @@ function main() {
811
825
  // 4. A controlled vocabulary decaying into free text.
812
826
  if (!status) {
813
827
  errors.push(
814
- `difference-list row ${id} has status "${statusCell}"; expected one of ${STATUSES.join(" / ")} (Locked 37)`,
828
+ `difference-list row ${id} has status "${statusCell}"; expected one of ${STATUSES.join(" / ")} (Locked 36)`,
815
829
  );
816
830
  continue;
817
831
  }
818
832
  // 5. The reason the mode exists: a behaviour v2 drops with no decision.
819
833
  if ((status === "Missing" || status === "Partial") && !/\bAS-\d{2,3}\b/.test(row)) {
820
834
  errors.push(
821
- `difference-list row ${id} is ${status} with no AS-NN open question (Locked 37)`,
835
+ `difference-list row ${id} is ${status} with no AS-NN open question (Locked 36)`,
822
836
  );
823
837
  }
824
838
  }
@@ -852,7 +866,7 @@ function main() {
852
866
  const cellCount = (line) =>
853
867
  line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").length;
854
868
  if (!header) {
855
- errors.push("Section 4.6 has no v1/v2 header row (Locked 37)");
869
+ errors.push("Section 4.6 has no v1/v2 header row (Locked 36)");
856
870
  } else {
857
871
  const cols = cellCount(header);
858
872
  for (const row of tableDataRows(epBody)) {
@@ -871,7 +885,7 @@ function main() {
871
885
  // keyword as a raw substring of the heading line - deliberately, since Locked
872
886
  // 2 re-flows section numbers and the corporate profile numbers them
873
887
  // differently - so an unrelated `### 4.9 Notes on the old Current Behaviour
874
- // audit process` matched, and Phase 4 runs --strict, which turns this warning
888
+ // audit process` matched, and Phase 3 runs --strict, which turns this warning
875
889
  // into a blocked document. The artefact itself is the honest signal: a
876
890
  // rendered current-behaviour table always carries `CB-<slug>-NN` ids, and a
877
891
  // paragraph that merely uses the words never does.
@@ -911,7 +925,7 @@ function main() {
911
925
 
912
926
  // 4. Placeholder whole-section bodies (Locked 2).
913
927
  // Not a defect in the corporate profile: there N/A and EKLENECEK are how a
914
- // backbone section reports "considered, nothing to state" (Locked 33), and the
928
+ // backbone section reports "considered, nothing to state" (Locked 32), and the
915
929
  // EKLENECEK-to-open-question pairing above is what keeps them honest.
916
930
  if (profile === "global" && /^\s*(TBD|Not applicable|N\/A)\s*$/im.test(text)) {
917
931
  warns.push(
@@ -921,7 +935,49 @@ function main() {
921
935
 
922
936
  mark("placeholder-bodies", profile === "global" ? undefined : "corporate profile");
923
937
 
924
- // 5. Traceability (Full mode only, Locked 31)
938
+ // 4b. Locked 2's numbering half, which was ungated until v19.0.0.
939
+ //
940
+ // It is NOT "numbering re-flows 1..N". That is what the rule used to say and
941
+ // it could not hold: Locked 30 threads ids across the document by section
942
+ // NUMBER ("Section 15.1 scenario", "Section 4.4", "Section 3/5 layout cells"),
943
+ // so a re-flowed document sends every one of those references to the wrong
944
+ // section. No emitted document ever re-flowed. The rule was the wrong half.
945
+ //
946
+ // What holds instead: a rendered section keeps its canonical template number,
947
+ // so gaps are correct and `1, 2, 4, 9, 13, 14, 21` is a valid document. Two
948
+ // things are still defects and both are checked here - a number outside the
949
+ // template's range, and numbers that repeat or go backwards. Either one means
950
+ // a cross-reference lands somewhere its author did not intend.
951
+ {
952
+ const MAX_SECTION = 23;
953
+ const nums = [];
954
+ for (const l of text.split("\n")) {
955
+ const m = /^##\s+(\d+)\.\s/.exec(l);
956
+ if (m) nums.push(Number(m[1]));
957
+ }
958
+ if (nums.length === 0) {
959
+ mark("section-numbering", "no numbered top-level sections");
960
+ } else {
961
+ const outOfRange = nums.filter((n) => n < 1 || n > MAX_SECTION);
962
+ const notAscending = [];
963
+ for (let i = 1; i < nums.length; i++) {
964
+ if (nums[i] <= nums[i - 1]) notAscending.push(`${nums[i - 1]} then ${nums[i]}`);
965
+ }
966
+ if (outOfRange.length > 0) {
967
+ errors.push(
968
+ `section number outside the 1..${MAX_SECTION} template range (Locked 2): ${outOfRange.join(", ")}`,
969
+ );
970
+ }
971
+ if (notAscending.length > 0) {
972
+ errors.push(
973
+ `section numbers repeat or go backwards (Locked 2): ${notAscending.slice(0, 4).join("; ")}`,
974
+ );
975
+ }
976
+ mark("section-numbering");
977
+ }
978
+ }
979
+
980
+ // 5. Traceability (Locked 30)
925
981
  if (mode === "full") {
926
982
  const idRe = /BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/gi;
927
983
  const all = text.match(idRe) || [];
@@ -931,18 +987,18 @@ function main() {
931
987
  counts.set(key, (counts.get(key) || 0) + 1);
932
988
  }
933
989
  if (counts.size === 0 && profile === "global") {
934
- warns.push("Full mode but no BR-<slug>-NN business-rule ids found (Section 4.4 / Locked 31)");
990
+ warns.push("no BR-<slug>-NN business-rule ids found (Section 4.4 / Locked 30)");
935
991
  }
936
992
  for (const [id, n] of counts) {
937
993
  if (n < 2) {
938
994
  warns.push(
939
- `business rule ${id} is defined but never referenced downstream (test/acceptance/analytics) - Locked 31 traceability`,
995
+ `business rule ${id} is defined but never referenced downstream (test/acceptance/analytics) - Locked 30 traceability`,
940
996
  );
941
997
  }
942
998
  }
943
999
  }
944
1000
 
945
- mark("br-reference-count", mode === "full" ? undefined : "lite mode");
1001
+ mark("br-reference-count");
946
1002
 
947
1003
  // 6. Manual test scenarios (Section 15.7): every MT row carries a BR id.
948
1004
  // A manual scenario nobody can trace back to a rule is a scenario nobody can
@@ -951,7 +1007,7 @@ function main() {
951
1007
  if (!/^\|\s*MT-[a-z0-9-]+/i.test(line)) continue;
952
1008
  if (!/BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/i.test(line)) {
953
1009
  const id = (line.match(/MT-[a-z0-9-]+/i) || ["MT row"])[0];
954
- errors.push(`manual scenario ${id} has no BR- id (Section 15.7 / Locked 31)`);
1010
+ errors.push(`manual scenario ${id} has no BR- id (Section 15.7 / Locked 30)`);
955
1011
  }
956
1012
  }
957
1013
 
@@ -978,13 +1034,188 @@ function main() {
978
1034
  if (/^-+$/.test(cells[1]) || /Bile\u015fen|Component/i.test(cells[1])) continue;
979
1035
  if (!cells[3] || !cells[4]) {
980
1036
  errors.push(
981
- `variant row "${cells[1]}" is missing the all-values or used-here column (Section 6.X / Locked 29)`,
1037
+ `variant row "${cells[1]}" is missing the all-values or used-here column (Section 6.X / Locked 28)`,
982
1038
  );
983
1039
  }
984
1040
  }
985
1041
 
986
1042
  mark("variant-coverage");
987
1043
 
1044
+ // 2h. Locked 13, 14, 15, 17, 18 and 21.
1045
+ //
1046
+ // Each checks a SHAPE the template prescribes. None judges content: a gate
1047
+ // that guesses at meaning fails honest documents, and a gate people learn to
1048
+ // ignore is worse than the prose it replaced.
1049
+ //
1050
+ // Each is keyed off a structure only a real document carries, so a minimal
1051
+ // document is skipped rather than failed. "Section 4 exists" is not evidence
1052
+ // that anybody wrote scenarios, and "Section 4 has a 4.1 sub-heading" is.
1053
+
1054
+ // Locked 13: Section 4 scenarios are Gherkin, not prose.
1055
+ const storiesBody = sectionBody(allLines, [
1056
+ "Kullanıcı Hikayeleri",
1057
+ "Kullanici Hikayeleri",
1058
+ "User Stories",
1059
+ ]);
1060
+ if (storiesBody) {
1061
+ const blocks = [];
1062
+ let inFence = false;
1063
+ let isGherkin = false;
1064
+ let buf = [];
1065
+ for (const line of storiesBody) {
1066
+ if (line.trim().startsWith("```")) {
1067
+ if (!inFence) {
1068
+ inFence = true;
1069
+ isGherkin = /^```\s*gherkin/i.test(line.trim());
1070
+ buf = [];
1071
+ } else {
1072
+ if (isGherkin) blocks.push(buf.join("\n"));
1073
+ inFence = false;
1074
+ }
1075
+ continue;
1076
+ }
1077
+ if (inFence && isGherkin) buf.push(line);
1078
+ }
1079
+ // Scenarios are 4.1 to 4.3. 4.4 is the business-rule table, and a redesign
1080
+ // run adds 4.5 and up (Locked 36: current behaviour, endpoint mapping,
1081
+ // difference list) - none of which is a scenario, so "everything except
1082
+ // 4.4" would demand Gherkin of three tables.
1083
+ const scenarioHeads = storiesBody.filter((l) => /^#{3}\s+4\.[1-3]\b/.test(l));
1084
+ if (scenarioHeads.length && !blocks.length) {
1085
+ errors.push(
1086
+ `Section 4 has ${scenarioHeads.length} scenario sub-section(s) and no \`\`\`gherkin block; plain-prose user stories are rejected (Locked 13)`,
1087
+ );
1088
+ }
1089
+ blocks.forEach((b, i) => {
1090
+ const missing = ["Given", "When", "Then"].filter(
1091
+ (k) => !new RegExp(`(^|\\n)\\s*${k}\\b`).test(b),
1092
+ );
1093
+ if (missing.length) {
1094
+ errors.push(`Section 4 gherkin block ${i + 1} states no ${missing.join(", ")} (Locked 13)`);
1095
+ }
1096
+ });
1097
+ }
1098
+
1099
+ mark("gherkin-scenarios", storiesBody ? undefined : "no User Stories section");
1100
+
1101
+ // Locked 14: Goals and Non-Goals are paired. A goal with an empty counterpart
1102
+ // cell is the failure the decision names - omission cannot be read as
1103
+ // exclusion, so the column has to say what is NOT covered.
1104
+ const goalsBody = sectionBody(allLines, [
1105
+ "Hedefler ve Karşı Hedefler",
1106
+ "Hedefler ve Karsi Hedefler",
1107
+ "Goals and Non-Goals",
1108
+ "Goals",
1109
+ ]);
1110
+ let goalRows = 0;
1111
+ if (goalsBody) {
1112
+ for (const row of tableDataRows(goalsBody)) {
1113
+ const cells = row
1114
+ .split("|")
1115
+ .map((c) => c.trim())
1116
+ .filter((c, i, a) => i !== 0 && i !== a.length - 1);
1117
+ if (cells.length < 2) continue;
1118
+ goalRows++;
1119
+ if (!cells[1] || cells[1] === "-") {
1120
+ errors.push(`goal "${cells[0].slice(0, 40)}" has no paired non-goal (Locked 14)`);
1121
+ }
1122
+ }
1123
+ }
1124
+
1125
+ mark("goals-paired", goalRows ? undefined : "no Goals/Non-Goals table");
1126
+
1127
+ // Locked 15: a new asset that is not SVG owes a rationale. The decision
1128
+ // allows Lottie and optimized PNG - it does not allow them silently.
1129
+ const assetsBody = sectionBody(allLines, ["Asset Envanteri", "Asset Inventory"]);
1130
+ let assetRows = 0;
1131
+ if (assetsBody) {
1132
+ for (const row of tableDataRows(assetsBody)) {
1133
+ const cells = row
1134
+ .split("|")
1135
+ .map((c) => c.trim())
1136
+ .filter((c, i, a) => i !== 0 && i !== a.length - 1);
1137
+ if (cells.length < 5) continue;
1138
+ assetRows++;
1139
+ const [name, format, existing, , notes] = cells;
1140
+ const isNew = /^(no|hayır|hayir)\b/i.test(existing);
1141
+ if (!isNew) continue;
1142
+ if (/\bsvg\b/i.test(format)) continue;
1143
+ if (!notes || !/\S/.test(notes.replace(/^new,?\s*/i, ""))) {
1144
+ errors.push(
1145
+ `new asset "${name.slice(0, 40)}" is ${format} rather than SVG and states no exception rationale (Locked 15)`,
1146
+ );
1147
+ }
1148
+ }
1149
+ }
1150
+
1151
+ mark("asset-format", assetRows ? undefined : "no Asset Inventory table");
1152
+
1153
+ // Locked 17: "other errors" is the phrase the decision exists to reject. An
1154
+ // exhaustive list cannot be checked for exhaustiveness from inside the
1155
+ // document, but the admission that it is not exhaustive can.
1156
+ const apiBody = sectionBody(allLines, ["API Kontratları", "API Kontratlari", "API Contracts"]);
1157
+ if (apiBody) {
1158
+ const VAGUE = [
1159
+ /\bother errors?\b/i,
1160
+ /\bvarious \dxx\b/i,
1161
+ /\bdiğer hatalar\b/i,
1162
+ /\bdiger hatalar\b/i,
1163
+ /\betc\.?\s*\|/i,
1164
+ ];
1165
+ for (const line of apiBody) {
1166
+ const hit = VAGUE.find((rx) => rx.test(line));
1167
+ if (hit) {
1168
+ errors.push(
1169
+ `Section 9 stands in for a status code instead of listing it: "${line.trim().slice(0, 60)}" (Locked 17)`,
1170
+ );
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ mark("api-variants-exhaustive", apiBody ? undefined : "no API Contracts section");
1176
+
1177
+ // Locked 18: screenshots are embedded, not linked. A remote image survives
1178
+ // exactly as long as whatever serves it, and the document outlives that.
1179
+ const designBody = sectionBody(allLines, [
1180
+ "Tasarım Referansı",
1181
+ "Tasarim Referansi",
1182
+ "Design Reference",
1183
+ ]);
1184
+ if (designBody) {
1185
+ for (const line of designBody) {
1186
+ const m = /!\[[^\]]*\]\((https?:\/\/[^)]+)\)/.exec(line);
1187
+ if (m) {
1188
+ errors.push(
1189
+ `Section 5 links a remote screenshot instead of embedding a local file: ${m[1].slice(0, 60)} (Locked 18)`,
1190
+ );
1191
+ }
1192
+ }
1193
+ }
1194
+
1195
+ mark("screenshots-embedded", designBody ? undefined : "no Design Reference section");
1196
+
1197
+ // Locked 21: References sit at the bottom. The reader sees goals, flow and
1198
+ // design before sources, which is only true if no numbered section follows.
1199
+ const topNums = [];
1200
+ let refNum = null;
1201
+ for (const line of allLines) {
1202
+ const m = /^##\s+(\d+)\.\s+(.*)$/.exec(line);
1203
+ if (!m) continue;
1204
+ const n = Number(m[1]);
1205
+ topNums.push(n);
1206
+ if (/Referanslar|References/i.test(m[2])) refNum = n;
1207
+ }
1208
+ if (refNum !== null && topNums.length) {
1209
+ const highest = Math.max(...topNums);
1210
+ if (refNum !== highest) {
1211
+ errors.push(
1212
+ `References is Section ${refNum} but Section ${highest} follows it, and sources go last (Locked 21)`,
1213
+ );
1214
+ }
1215
+ }
1216
+
1217
+ mark("references-last", refNum === null ? "no References section" : undefined);
1218
+
988
1219
  // Report
989
1220
  if (report) {
990
1221
  const width = Math.max(...checkRows.map((r) => r.id.length));
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // validate-planning.mjs - runtime validator for Phase 2 planning output.
2
+ // validate-planning.mjs - runtime validator for Phase 1 planning output.
3
3
  //
4
4
  // Phase 2 returns a task decomposition conforming to
5
5
  // pipeline/schemas/planning-output.schema.json. Phase 3 iterates over `tasks`
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // validate-reviewer.mjs - runtime validator for Phase 4 reviewer subagent output.
2
+ // validate-reviewer.mjs - runtime validator for Phase 3 reviewer subagent output.
3
3
  //
4
4
  // Each of the 3 parallel reviewers (Opus, GPT-5.4, Sonnet) must return an
5
5
  // object matching pipeline/schemas/reviewer-output.schema.json. This validator
@@ -14,7 +14,7 @@
14
14
  // actually make a resume unsafe:
15
15
  // - the file does not parse (truncated / corrupt)
16
16
  // - root is not an object
17
- // - currentPhase is missing or out of the 0..7 range (resume keys on it)
17
+ // - currentPhase is missing or out of the 0..5 range (resume keys on it)
18
18
  // - phases is present but not an object, or an entry is not an object
19
19
  // Phase status strings are NOT enum-checked: real writers use "completed"
20
20
  // alongside the schema's "done", and an odd status does not make a resume
@@ -66,9 +66,33 @@ async function main() {
66
66
  process.exit(1);
67
67
  }
68
68
 
69
+ // Which numbering this file was written under. v19.0.0 renumbered eight
70
+ // phases into six, and a run that finished at `currentPhase: 7` in October
71
+ // was correct when it was written - calling it invalid now would mark most
72
+ // of the log directory broken and teach the reader to ignore this validator.
73
+ // Same decision the metrics file got: label the generation, do not rewrite
74
+ // history. A file stamped 2.2.0 or later is generation 2 and bounded 0..5;
75
+ // anything older, including the unstamped files that predate stamping
76
+ // entirely, is generation 1 and bounded 0..7.
77
+ const GEN2_FROM = "2.2.0";
78
+ const cmpVersion = (a, b) => {
79
+ const pa = String(a).split(".").map(Number);
80
+ const pb = String(b).split(".").map(Number);
81
+ for (let i = 0; i < 3; i++) {
82
+ if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
83
+ }
84
+ return 0;
85
+ };
86
+ const gen1 = !s.schemaVersion || cmpVersion(s.schemaVersion, GEN2_FROM) < 0;
87
+ const maxPhase = gen1 ? 7 : 5;
88
+ const phaseKey = gen1 ? /^[0-7]$/ : /^[0-5]$/;
89
+
69
90
  // currentPhase is the field resume keys on - it must be present and in range.
70
- if (!Number.isInteger(s.currentPhase) || s.currentPhase < 0 || s.currentPhase > 7) {
71
- errors.push(`currentPhase must be integer 0..7, got ${JSON.stringify(s.currentPhase)}`);
91
+ if (!Number.isInteger(s.currentPhase) || s.currentPhase < 0 || s.currentPhase > maxPhase) {
92
+ errors.push(
93
+ `currentPhase must be integer 0..${maxPhase}, got ${JSON.stringify(s.currentPhase)}` +
94
+ (gen1 ? " (generation 1 file: unstamped or schemaVersion < 2.2.0)" : ""),
95
+ );
72
96
  }
73
97
 
74
98
  // phases is optional (some early/aborted states omit it), but if present it
@@ -78,12 +102,28 @@ async function main() {
78
102
  errors.push("phases, when present, must be an object keyed by phase number");
79
103
  } else {
80
104
  for (const [k, v] of Object.entries(s.phases)) {
81
- if (!/^[0-7]$/.test(k)) {
82
- errors.push(`phases key invalid: "${k}" (expected "0".."7")`);
105
+ if (!phaseKey.test(k)) {
106
+ errors.push(`phases key invalid: "${k}" (expected "0".."${maxPhase}")`);
83
107
  continue;
84
108
  }
85
109
  if (typeof v !== "object" || v === null) {
86
110
  errors.push(`phases["${k}"] must be an object`);
111
+ continue;
112
+ }
113
+ // The rework cap. Every phase doc says "hard-kill at retryCount === 3"
114
+ // and the schema writes `maximum: 3`, but nothing read either: a fourth
115
+ // round produced a state that violates the schema and passed this
116
+ // validator, which is the one place that could have said so.
117
+ //
118
+ // What this does NOT do is stop the loop - that is the orchestrator's
119
+ // job and it stays prose. What it does is make an over-cap state fail
120
+ // instead of being carried forward as if it were fine.
121
+ if (v.retryCount !== undefined) {
122
+ if (!Number.isInteger(v.retryCount) || v.retryCount < 0 || v.retryCount > 3) {
123
+ errors.push(
124
+ `phases["${k}"].retryCount must be integer 0..3 (the rework cap), got ${JSON.stringify(v.retryCount)}`,
125
+ );
126
+ }
87
127
  }
88
128
  }
89
129
  }