@jamie-tam/forge 6.0.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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +389 -0
  3. package/agents/architect.md +92 -0
  4. package/agents/builder.md +122 -0
  5. package/agents/code-reviewer.md +107 -0
  6. package/agents/concept-designer.md +207 -0
  7. package/agents/craft-reviewer.md +132 -0
  8. package/agents/critic.md +130 -0
  9. package/agents/doc-writer.md +85 -0
  10. package/agents/dreamer.md +129 -0
  11. package/agents/e2e-runner.md +89 -0
  12. package/agents/gotcha-hunter.md +127 -0
  13. package/agents/prototype-builder.md +193 -0
  14. package/agents/prototype-codifier.md +204 -0
  15. package/agents/prototype-reviewer.md +163 -0
  16. package/agents/security-reviewer.md +108 -0
  17. package/agents/spec-reviewer.md +94 -0
  18. package/agents/tracer.md +98 -0
  19. package/agents/wireframer.md +109 -0
  20. package/commands/abort.md +25 -0
  21. package/commands/bugfix.md +151 -0
  22. package/commands/evolve.md +118 -0
  23. package/commands/feature.md +236 -0
  24. package/commands/forge.md +100 -0
  25. package/commands/greenfield.md +185 -0
  26. package/commands/hotfix.md +98 -0
  27. package/commands/refactor.md +147 -0
  28. package/commands/resume.md +25 -0
  29. package/commands/setup.md +201 -0
  30. package/commands/status.md +27 -0
  31. package/commands/task-force.md +110 -0
  32. package/commands/validate.md +12 -0
  33. package/dist/__tests__/active-manifest.test.js +272 -0
  34. package/dist/__tests__/copy.test.js +96 -0
  35. package/dist/__tests__/gate-check.test.js +384 -0
  36. package/dist/__tests__/wiki.test.js +472 -0
  37. package/dist/__tests__/work-manifest.test.js +304 -0
  38. package/dist/active-manifest.js +229 -0
  39. package/dist/cli.js +158 -0
  40. package/dist/copy.js +124 -0
  41. package/dist/gate-check.js +326 -0
  42. package/dist/hooks.js +60 -0
  43. package/dist/init.js +140 -0
  44. package/dist/manifest.js +90 -0
  45. package/dist/merge.js +77 -0
  46. package/dist/paths.js +36 -0
  47. package/dist/uninstall.js +216 -0
  48. package/dist/update.js +158 -0
  49. package/dist/verify-manifest.js +65 -0
  50. package/dist/verify.js +98 -0
  51. package/dist/wiki-ui.js +310 -0
  52. package/dist/wiki.js +364 -0
  53. package/dist/work-manifest.js +798 -0
  54. package/hooks/config/gate-requirements.json +79 -0
  55. package/hooks/hooks.json +143 -0
  56. package/hooks/scripts/analyze-telemetry.sh +114 -0
  57. package/hooks/scripts/gate-enforcer.sh +164 -0
  58. package/hooks/scripts/pre-compact.sh +90 -0
  59. package/hooks/scripts/session-start.sh +81 -0
  60. package/hooks/scripts/telemetry.sh +41 -0
  61. package/hooks/scripts/wiki-lint.sh +87 -0
  62. package/hooks/templates/AGENTS.md.template +48 -0
  63. package/hooks/templates/CLAUDE.md.template +45 -0
  64. package/package.json +55 -0
  65. package/protocols/README.md +40 -0
  66. package/protocols/codex.md +151 -0
  67. package/protocols/graphify.md +156 -0
  68. package/references/common/agent-coordination.md +65 -0
  69. package/references/common/coding-standards.md +54 -0
  70. package/references/common/feature-tracking.md +21 -0
  71. package/references/common/io-protocol.md +36 -0
  72. package/references/common/phases.md +57 -0
  73. package/references/common/quality-gates.md +130 -0
  74. package/references/common/skill-authoring.md +154 -0
  75. package/references/common/skill-compliance.md +30 -0
  76. package/references/python/standards.md +44 -0
  77. package/references/react/standards.md +61 -0
  78. package/references/typescript/standards.md +42 -0
  79. package/rules/common/forge-system.md +59 -0
  80. package/rules/common/git-workflow.md +40 -0
  81. package/rules/common/guardrails.md +37 -0
  82. package/rules/common/quality-gates.md +18 -0
  83. package/rules/common/security.md +50 -0
  84. package/rules/common/skill-selection.md +78 -0
  85. package/rules/common/testing.md +58 -0
  86. package/rules/common/verification.md +39 -0
  87. package/skills/build-pr-workflow/SKILL.md +301 -0
  88. package/skills/build-pr-workflow/references/pr-template.md +62 -0
  89. package/skills/build-pr-workflow/references/subagent-merge.md +47 -0
  90. package/skills/build-pr-workflow/references/worktree-setup.md +125 -0
  91. package/skills/build-prototype/SKILL.md +264 -0
  92. package/skills/build-scaffold/SKILL.md +340 -0
  93. package/skills/build-tdd/SKILL.md +89 -0
  94. package/skills/build-wireframe/SKILL.md +110 -0
  95. package/skills/build-wireframe/assets/baseline-template.html +486 -0
  96. package/skills/build-wireframe/references/demo-walkthroughs.md +170 -0
  97. package/skills/build-wireframe/references/gotchas.md +188 -0
  98. package/skills/build-wireframe/references/legend-lines.md +141 -0
  99. package/skills/concept-slides/SKILL.md +192 -0
  100. package/skills/deliver-db-migration/SKILL.md +466 -0
  101. package/skills/deliver-deploy/SKILL.md +407 -0
  102. package/skills/deliver-onboarding/SKILL.md +198 -0
  103. package/skills/deliver-onboarding/references/document-templates.md +393 -0
  104. package/skills/deliver-onboarding/templates/getting-started.md +122 -0
  105. package/skills/discover-codebase-analysis/SKILL.md +448 -0
  106. package/skills/discover-requirements/SKILL.md +418 -0
  107. package/skills/discover-requirements/templates/prd.md +99 -0
  108. package/skills/discover-requirements/templates/technical-spec.md +123 -0
  109. package/skills/discover-requirements/templates/user-stories.md +76 -0
  110. package/skills/harden/SKILL.md +214 -0
  111. package/skills/iterate-prototype/SKILL.md +241 -0
  112. package/skills/plan-architecture/SKILL.md +457 -0
  113. package/skills/plan-architecture/templates/adr-template.md +52 -0
  114. package/skills/plan-architecture/templates/api-contract.md +99 -0
  115. package/skills/plan-architecture/templates/db-schema.md +81 -0
  116. package/skills/plan-architecture/templates/system-design.md +111 -0
  117. package/skills/plan-brainstorm/SKILL.md +433 -0
  118. package/skills/plan-design-system/SKILL.md +279 -0
  119. package/skills/plan-task-decompose/SKILL.md +454 -0
  120. package/skills/quality-code-review/SKILL.md +286 -0
  121. package/skills/quality-security-audit/SKILL.md +292 -0
  122. package/skills/quality-security-audit/references/audit-report-template.md +89 -0
  123. package/skills/quality-security-audit/references/owasp-checks.md +178 -0
  124. package/skills/quality-test-execution/SKILL.md +435 -0
  125. package/skills/quality-test-plan/SKILL.md +297 -0
  126. package/skills/quality-test-plan/references/test-type-guide.md +263 -0
  127. package/skills/quality-test-plan/templates/e2e-test-plan.md +72 -0
  128. package/skills/quality-test-plan/templates/integration-test-plan.md +74 -0
  129. package/skills/quality-test-plan/templates/load-test-plan.md +111 -0
  130. package/skills/quality-test-plan/templates/smoke-test-plan.md +68 -0
  131. package/skills/quality-test-plan/templates/unit-test-plan.md +56 -0
  132. package/skills/quality-uiux/SKILL.md +481 -0
  133. package/skills/support-debug/SKILL.md +464 -0
  134. package/skills/support-dream/SKILL.md +213 -0
  135. package/skills/support-gotcha/SKILL.md +249 -0
  136. package/skills/support-runtime-reachability/SKILL.md +190 -0
  137. package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/app.ts +7 -0
  138. package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/handlers/cases.ts +7 -0
  139. package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/app.ts +8 -0
  140. package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/handlers/cases.ts +7 -0
  141. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/App.tsx +5 -0
  142. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/components/RingingBanner.tsx +7 -0
  143. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/hooks/useTwilio.ts +6 -0
  144. package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/App.tsx +5 -0
  145. package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/components/MyComp.tsx +3 -0
  146. package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/App.tsx +3 -0
  147. package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/components/Orphan.tsx +3 -0
  148. package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/lib/Service.ts +6 -0
  149. package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/main.ts +4 -0
  150. package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/lib/Lonely.ts +5 -0
  151. package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/main.ts +2 -0
  152. package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/handler.ts +3 -0
  153. package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/main.ts +3 -0
  154. package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/handler.ts +3 -0
  155. package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/main.ts +2 -0
  156. package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/lib.ts +5 -0
  157. package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/main.ts +3 -0
  158. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/index.ts +1 -0
  159. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/internal.ts +3 -0
  160. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/main.ts +3 -0
  161. package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.test.ts +5 -0
  162. package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.ts +3 -0
  163. package/skills/support-runtime-reachability/scripts/__fixtures__/case-13-gated-pending-annotation/src/future.ts +4 -0
  164. package/skills/support-runtime-reachability/scripts/__fixtures__/case-14-untraceable-annotation/src/decorated.ts +4 -0
  165. package/skills/support-runtime-reachability/scripts/__fixtures__/case-15-untraceable-empty/src/lazy.ts +4 -0
  166. package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/lib.py +15 -0
  167. package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/main.py +5 -0
  168. package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/parent.ts +5 -0
  169. package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/routes/cases.ts +5 -0
  170. package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/lib/foo.ts +3 -0
  171. package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/other.ts +8 -0
  172. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/cases.ts +4 -0
  173. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/users.ts +4 -0
  174. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/main.ts +5 -0
  175. package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/handlers/cases.ts +3 -0
  176. package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/main.ts +4 -0
  177. package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/lib.ts +5 -0
  178. package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/main.ts +5 -0
  179. package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/lib.ts +3 -0
  180. package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/main.ts +8 -0
  181. package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/lib.ts +3 -0
  182. package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/main.ts +7 -0
  183. package/skills/support-runtime-reachability/scripts/check.mjs +638 -0
  184. package/skills/support-runtime-reachability/scripts/check.test.mjs +244 -0
  185. package/skills/support-skill-validator/SKILL.md +194 -0
  186. package/skills/support-skill-validator/references/false-positives.md +59 -0
  187. package/skills/support-skill-validator/references/validation-checks.md +280 -0
  188. package/skills/support-system-guide/SKILL.md +311 -0
  189. package/skills/support-task-force/SKILL.md +265 -0
  190. package/skills/support-task-force/references/dispatch-pattern.md +178 -0
  191. package/skills/support-task-force/references/synthesis-template.md +126 -0
  192. package/skills/support-wiki-bootstrap/SKILL.md +37 -0
  193. package/skills/support-wiki-lint/SKILL.md +196 -0
  194. package/skills/support-wiki-lint/scripts/lint.mjs +488 -0
  195. package/skills/support-wiki-lint/scripts/lint.test.mjs +196 -0
  196. package/templates/README.md +23 -0
  197. package/templates/aiwiki/CLAUDE.md.template +78 -0
  198. package/templates/aiwiki/schemas/architecture.md +118 -0
  199. package/templates/aiwiki/schemas/convention.md +112 -0
  200. package/templates/aiwiki/schemas/decision.md +144 -0
  201. package/templates/aiwiki/schemas/gotcha.md +118 -0
  202. package/templates/aiwiki/schemas/oracle.md +105 -0
  203. package/templates/aiwiki/schemas/session.md +125 -0
  204. package/templates/manifests/bugfix.yaml +41 -0
  205. package/templates/manifests/feature.yaml +69 -0
  206. package/templates/manifests/greenfield.yaml +61 -0
  207. package/templates/manifests/hotfix.yaml +45 -0
  208. package/templates/manifests/refactor.yaml +44 -0
  209. package/templates/manifests/v5/SCHEMA.md +327 -0
  210. package/templates/manifests/v5/feature.yaml +77 -0
  211. package/templates/manifests/v6/SCHEMA.md +199 -0
  212. package/templates/wiki-html/dream-detail.html +378 -0
  213. package/templates/wiki-html/dreams-list.html +155 -0
@@ -0,0 +1,798 @@
1
+ /**
2
+ * v6 work-manifest verifier.
3
+ *
4
+ * Specs:
5
+ * - v6 delta: templates/manifests/v6/SCHEMA.md (phase_plan extension)
6
+ * - Base structure: templates/manifests/v5/SCHEMA.md (slice graph, gates)
7
+ *
8
+ * Distinct from src/manifest.ts (which is the install manifest, forge.json).
9
+ *
10
+ * v4 manifests parse and are synthesized to a v5-shaped in-memory structure
11
+ * per v5 SCHEMA.md §6. v5 manifests parse as-is with `phase_plan` undefined.
12
+ * v6 manifests are parsed natively with phase_plan normalized to object form.
13
+ */
14
+ import { readFileSync } from "node:fs";
15
+ import { load as yamlLoad } from "js-yaml";
16
+ // ============================================================================
17
+ // Constants (per SCHEMA.md §3.4)
18
+ // ============================================================================
19
+ const RESERVED_MANIFEST_GATE_NAMES = new Set([
20
+ "codebase-analysis",
21
+ "brainstorm",
22
+ "design-system",
23
+ "architecture",
24
+ "task-decompose",
25
+ "code-review-final",
26
+ "test-plan",
27
+ "test-execution",
28
+ "uiux-review",
29
+ ]);
30
+ const ALLOWED_SLICE_GATE_NAMES = new Set([
31
+ "skeleton-runs",
32
+ "build-tdd",
33
+ "wiki-lint",
34
+ "runtime-reach",
35
+ "code-review",
36
+ ]);
37
+ const VALID_PHASE_PLAN_STATUSES = [
38
+ "active",
39
+ "active-light",
40
+ "active-commit-only",
41
+ "skipped",
42
+ "as-discovered",
43
+ "complete-inline",
44
+ ];
45
+ const SLICE_ID_RE = /^[a-z][a-z0-9-]*$/;
46
+ const VALID_SLICE_TYPES = ["skeleton", "feature-slice", "refactor-slice"];
47
+ const VALID_SLICE_STATUSES = [
48
+ "pending",
49
+ "in-progress",
50
+ "gated",
51
+ "complete",
52
+ "abandoned",
53
+ ];
54
+ const VALID_GATE_STATUSES = [
55
+ "pending",
56
+ "in-progress",
57
+ "complete",
58
+ "skipped",
59
+ "not-applicable",
60
+ ];
61
+ const VALID_MANIFEST_TYPES = [
62
+ "feature",
63
+ "bugfix",
64
+ "hotfix",
65
+ "refactor",
66
+ "greenfield",
67
+ ];
68
+ const VALID_MANIFEST_STATUSES = [
69
+ "in-progress",
70
+ "paused",
71
+ "completed",
72
+ "escalated",
73
+ "abandoned",
74
+ ];
75
+ // Per-work-type recommended phase_plan keys, sourced from v6 SCHEMA.md §3.2's
76
+ // recommended-keys table. Keys NOT in this set produce W_UNKNOWN_PHASE_PLAN_KEY
77
+ // warnings (advisory only — typos are silent failures by spec, so a warning
78
+ // gives commands a chance to catch them at preflight without breaking workflows
79
+ // that legitimately extend their plan with custom keys).
80
+ const RECOMMENDED_PHASE_PLAN_KEYS = new Map([
81
+ [
82
+ "feature",
83
+ new Set([
84
+ "discover-codebase",
85
+ "concept",
86
+ "wireframe",
87
+ "plan-design-system",
88
+ "prototype",
89
+ "iterate",
90
+ "codify",
91
+ "worktree",
92
+ "production-build",
93
+ "test-plan",
94
+ "uiux-review",
95
+ "code-review-final",
96
+ "deliver",
97
+ "onboarding",
98
+ "gotchas",
99
+ ]),
100
+ ],
101
+ [
102
+ "greenfield",
103
+ new Set([
104
+ "discover-requirements",
105
+ "concept",
106
+ "wireframe",
107
+ "plan-design-system",
108
+ "prototype",
109
+ "iterate",
110
+ "codify",
111
+ "scaffold",
112
+ "production-build",
113
+ "test-plan",
114
+ "uiux-review",
115
+ "code-review-final",
116
+ "deliver",
117
+ "onboarding",
118
+ "gotchas",
119
+ ]),
120
+ ],
121
+ [
122
+ "bugfix",
123
+ new Set(["debug-root-cause", "production-build", "code-review", "deliver", "gotchas"]),
124
+ ],
125
+ [
126
+ "hotfix",
127
+ new Set([
128
+ "debug-root-cause",
129
+ "production-build",
130
+ "smoke-tests",
131
+ "code-review-critical",
132
+ "deliver",
133
+ "gotchas",
134
+ "followup-ticket",
135
+ ]),
136
+ ],
137
+ [
138
+ "refactor",
139
+ new Set([
140
+ "discover-codebase",
141
+ "brainstorm",
142
+ "task-decompose",
143
+ "production-build",
144
+ "test-execution",
145
+ "assessment",
146
+ "deliver",
147
+ "gotchas",
148
+ ]),
149
+ ],
150
+ ]);
151
+ // ============================================================================
152
+ // Public API
153
+ // ============================================================================
154
+ export function parseManifest(yaml) {
155
+ let parsed;
156
+ try {
157
+ parsed = yamlLoad(yaml);
158
+ }
159
+ catch (e) {
160
+ const err = e;
161
+ return {
162
+ ok: false,
163
+ errors: [
164
+ {
165
+ code: "E_YAML_PARSE",
166
+ message: err.message ?? String(err),
167
+ path: "(root)",
168
+ },
169
+ ],
170
+ warnings: [],
171
+ };
172
+ }
173
+ if (!parsed || typeof parsed !== "object") {
174
+ return {
175
+ ok: false,
176
+ errors: [
177
+ { code: "E_YAML_PARSE", message: "manifest is empty or not an object", path: "(root)" },
178
+ ],
179
+ warnings: [],
180
+ };
181
+ }
182
+ const root = parsed;
183
+ // Normalize schema_version: YAML may parse a quoted "6" as string "6", but an
184
+ // unquoted 6 as number 6. Treat both as v6.
185
+ const schemaVersion = root.schema_version !== undefined ? String(root.schema_version) : "";
186
+ const isV6 = schemaVersion === "6";
187
+ const isV5 = schemaVersion === "5";
188
+ const hasV4Build = root.phases && typeof root.phases === "object" && "build" in root.phases;
189
+ let manifest;
190
+ let detectedSchema;
191
+ if (isV6) {
192
+ detectedSchema = "v6";
193
+ manifest = root;
194
+ }
195
+ else if (isV5) {
196
+ detectedSchema = "v5";
197
+ manifest = root;
198
+ }
199
+ else if (hasV4Build) {
200
+ detectedSchema = "v4";
201
+ manifest = synthesizeV5FromV4(root);
202
+ }
203
+ else {
204
+ return {
205
+ ok: false,
206
+ errors: [
207
+ {
208
+ code: "E_BAD_ENUM_VALUE",
209
+ message: `unrecognised schema_version "${schemaVersion}" and no v4 phases.build present`,
210
+ path: "schema_version",
211
+ },
212
+ ],
213
+ warnings: [],
214
+ };
215
+ }
216
+ const errors = [];
217
+ const warnings = [];
218
+ validateRequiredFields(manifest, errors, detectedSchema);
219
+ validateEnums(manifest, errors);
220
+ validateSliceGraph(manifest, errors);
221
+ validatePhaseGatePremature(manifest, errors);
222
+ if (detectedSchema === "v6") {
223
+ validateAndNormalizePhasePlan(manifest, errors);
224
+ validatePhasePlanKeysForType(manifest, warnings);
225
+ }
226
+ if (errors.length > 0) {
227
+ return { ok: false, errors, warnings };
228
+ }
229
+ return { ok: true, manifest, schema: detectedSchema, warnings };
230
+ }
231
+ export function readManifest(path) {
232
+ let yaml;
233
+ try {
234
+ yaml = readFileSync(path, "utf-8");
235
+ }
236
+ catch (e) {
237
+ return {
238
+ ok: false,
239
+ errors: [
240
+ {
241
+ code: "E_FILE_NOT_FOUND",
242
+ message: `cannot read ${path}: ${e.message}`,
243
+ path,
244
+ },
245
+ ],
246
+ warnings: [],
247
+ };
248
+ }
249
+ return parseManifest(yaml);
250
+ }
251
+ // ============================================================================
252
+ // Structural validators
253
+ // ============================================================================
254
+ function validateRequiredFields(manifest, errors, schema) {
255
+ // slice_graph is optional in v6 (planning is allowed before slice
256
+ // decomposition has happened — codify fills it in). v4 and v5 manifests
257
+ // always have a slice_graph: v4 synthesizes one, v5 templates ship with
258
+ // a populated graph.
259
+ const required = [
260
+ "schema_version",
261
+ "name",
262
+ "type",
263
+ "description",
264
+ "status",
265
+ "created",
266
+ "command",
267
+ "phases",
268
+ ...(schema === "v6" ? [] : ["slice_graph"]),
269
+ ];
270
+ for (const field of required) {
271
+ if (manifest[field] === undefined || manifest[field] === null) {
272
+ errors.push({
273
+ code: "E_MISSING_REQUIRED_FIELD",
274
+ message: `required field "${field}" is missing`,
275
+ path: String(field),
276
+ });
277
+ }
278
+ }
279
+ if (schema === "v6" && (manifest.phase_plan === undefined || manifest.phase_plan === null)) {
280
+ errors.push({
281
+ code: "E_MISSING_REQUIRED_FIELD",
282
+ message: `v6 manifests require a "phase_plan" block`,
283
+ path: "phase_plan",
284
+ });
285
+ }
286
+ // Shape checks (per SCHEMA.md §1)
287
+ if (manifest.name && !SLICE_ID_RE.test(String(manifest.name))) {
288
+ errors.push({
289
+ code: "E_BAD_ENUM_VALUE",
290
+ message: `name "${manifest.name}" must be kebab-case (^[a-z][a-z0-9-]*$)`,
291
+ path: "name",
292
+ });
293
+ }
294
+ if (manifest.phases !== undefined && manifest.phases !== null) {
295
+ if (typeof manifest.phases !== "object" || Array.isArray(manifest.phases)) {
296
+ errors.push({
297
+ code: "E_MISSING_REQUIRED_FIELD",
298
+ message: `phases must be a map`,
299
+ path: "phases",
300
+ });
301
+ }
302
+ }
303
+ if (manifest.slice_graph !== undefined && manifest.slice_graph !== null) {
304
+ const sg = manifest.slice_graph;
305
+ if (typeof sg !== "object" || Array.isArray(sg) || !sg.slices) {
306
+ errors.push({
307
+ code: "E_MISSING_REQUIRED_FIELD",
308
+ message: `slice_graph must be a map containing "slices"`,
309
+ path: "slice_graph",
310
+ });
311
+ }
312
+ else {
313
+ const slices = sg.slices;
314
+ if (typeof slices !== "object" || Array.isArray(slices)) {
315
+ errors.push({
316
+ code: "E_MISSING_REQUIRED_FIELD",
317
+ message: `slice_graph.slices must be a map`,
318
+ path: "slice_graph.slices",
319
+ });
320
+ }
321
+ const cs = sg.current_slice;
322
+ if (cs === undefined || cs === null || cs === "") {
323
+ errors.push({
324
+ code: "E_BAD_CURRENT_SLICE",
325
+ message: `slice_graph.current_slice is required and must reference an existing slice`,
326
+ path: "slice_graph.current_slice",
327
+ });
328
+ }
329
+ }
330
+ }
331
+ }
332
+ function validateEnums(manifest, errors) {
333
+ if (manifest.type && !VALID_MANIFEST_TYPES.includes(manifest.type)) {
334
+ errors.push({
335
+ code: "E_BAD_ENUM_VALUE",
336
+ message: `type "${manifest.type}" is not one of ${VALID_MANIFEST_TYPES.join(" | ")}`,
337
+ path: "type",
338
+ });
339
+ }
340
+ if (manifest.status && !VALID_MANIFEST_STATUSES.includes(manifest.status)) {
341
+ errors.push({
342
+ code: "E_BAD_ENUM_VALUE",
343
+ message: `status "${manifest.status}" is not one of ${VALID_MANIFEST_STATUSES.join(" | ")}`,
344
+ path: "status",
345
+ });
346
+ }
347
+ }
348
+ function validateSliceGraph(manifest, errors) {
349
+ const sg = manifest.slice_graph;
350
+ if (!sg || typeof sg !== "object" || !sg.slices)
351
+ return; // missing-required already caught
352
+ const sliceIds = Object.keys(sg.slices);
353
+ // §3.1 slice IDs must match regex
354
+ for (const id of sliceIds) {
355
+ if (!SLICE_ID_RE.test(id)) {
356
+ errors.push({
357
+ code: "E_BAD_SLICE_ID",
358
+ message: `slice ID "${id}" must match ^[a-z][a-z0-9-]*$`,
359
+ path: `slice_graph.slices.${id}`,
360
+ });
361
+ }
362
+ }
363
+ // §3.1 current_slice must resolve
364
+ if (sg.current_slice && !sg.slices[sg.current_slice]) {
365
+ errors.push({
366
+ code: "E_BAD_CURRENT_SLICE",
367
+ message: `current_slice "${sg.current_slice}" does not resolve to any slice`,
368
+ path: "slice_graph.current_slice",
369
+ });
370
+ }
371
+ // §3.2 per-slice invariants
372
+ const skeletons = [];
373
+ for (const [id, slice] of Object.entries(sg.slices)) {
374
+ const slicePath = `slice_graph.slices.${id}`;
375
+ if (!slice || typeof slice !== "object") {
376
+ errors.push({
377
+ code: "E_MISSING_REQUIRED_FIELD",
378
+ message: `slice "${id}" must be an object`,
379
+ path: slicePath,
380
+ });
381
+ continue;
382
+ }
383
+ if (!VALID_SLICE_TYPES.includes(slice.type)) {
384
+ errors.push({
385
+ code: "E_BAD_ENUM_VALUE",
386
+ message: `slice "${id}" type "${slice.type}" is not one of ${VALID_SLICE_TYPES.join(" | ")}`,
387
+ path: `${slicePath}.type`,
388
+ });
389
+ }
390
+ if (!VALID_SLICE_STATUSES.includes(slice.status)) {
391
+ errors.push({
392
+ code: "E_BAD_ENUM_VALUE",
393
+ message: `slice "${id}" status "${slice.status}" is not one of ${VALID_SLICE_STATUSES.join(" | ")}`,
394
+ path: `${slicePath}.status`,
395
+ });
396
+ }
397
+ if (slice.type === "skeleton") {
398
+ skeletons.push(id);
399
+ if (!slice.variant || typeof slice.variant !== "string" || slice.variant.trim() === "") {
400
+ errors.push({
401
+ code: "E_VARIANT_REQUIRED",
402
+ message: `skeleton slice "${id}" requires a non-empty variant`,
403
+ path: `${slicePath}.variant`,
404
+ });
405
+ }
406
+ if (slice.depends_on && slice.depends_on.length > 0) {
407
+ errors.push({
408
+ code: "E_SKELETON_NOT_ROOT",
409
+ message: `skeleton slice "${id}" must have empty depends_on (it is the root)`,
410
+ path: `${slicePath}.depends_on`,
411
+ });
412
+ }
413
+ }
414
+ else {
415
+ if (slice.variant !== undefined && slice.variant !== null) {
416
+ // §3.2 #8: variant must be ABSENT or null. Empty string is forbidden.
417
+ errors.push({
418
+ code: "E_VARIANT_FORBIDDEN",
419
+ message: `non-skeleton slice "${id}" must not have a variant (got: ${JSON.stringify(slice.variant)})`,
420
+ path: `${slicePath}.variant`,
421
+ });
422
+ }
423
+ }
424
+ // depends_on: shape + unresolved + self
425
+ if (slice.depends_on !== undefined && !Array.isArray(slice.depends_on)) {
426
+ errors.push({
427
+ code: "E_MISSING_REQUIRED_FIELD",
428
+ message: `slice "${id}" depends_on must be an array (got ${typeof slice.depends_on})`,
429
+ path: `${slicePath}.depends_on`,
430
+ });
431
+ // Replace with empty so cycle detection doesn't crash on this slice.
432
+ slice.depends_on = [];
433
+ }
434
+ const deps = Array.isArray(slice.depends_on) ? slice.depends_on : [];
435
+ for (let i = 0; i < deps.length; i++) {
436
+ const dep = deps[i];
437
+ if (dep === id) {
438
+ errors.push({
439
+ code: "E_SELF_DEP",
440
+ message: `slice "${id}" cannot depend on itself`,
441
+ path: `${slicePath}.depends_on[${i}]`,
442
+ });
443
+ }
444
+ if (!sg.slices[dep]) {
445
+ errors.push({
446
+ code: "E_UNRESOLVED_DEP",
447
+ message: `slice "${id}" depends on "${dep}" which does not exist`,
448
+ path: `${slicePath}.depends_on[${i}]`,
449
+ });
450
+ }
451
+ }
452
+ // §3.4 gate name collisions
453
+ if (slice.gates === undefined || slice.gates === null) {
454
+ errors.push({
455
+ code: "E_MISSING_REQUIRED_FIELD",
456
+ message: `slice "${id}" is missing required gates map`,
457
+ path: `${slicePath}.gates`,
458
+ });
459
+ }
460
+ else if (typeof slice.gates !== "object" || Array.isArray(slice.gates)) {
461
+ errors.push({
462
+ code: "E_MISSING_REQUIRED_FIELD",
463
+ message: `slice "${id}" gates must be a map, not ${Array.isArray(slice.gates) ? "array" : typeof slice.gates}`,
464
+ path: `${slicePath}.gates`,
465
+ });
466
+ }
467
+ else {
468
+ for (const [gateName, gate] of Object.entries(slice.gates)) {
469
+ const gatePath = `${slicePath}.gates.${gateName}`;
470
+ // Name validation
471
+ if (RESERVED_MANIFEST_GATE_NAMES.has(gateName)) {
472
+ errors.push({
473
+ code: "E_GATE_COLLISION",
474
+ message: `slice "${id}" gate "${gateName}" collides with a reserved manifest-level gate name`,
475
+ path: gatePath,
476
+ });
477
+ }
478
+ else if (!ALLOWED_SLICE_GATE_NAMES.has(gateName)) {
479
+ errors.push({
480
+ code: "E_BAD_ENUM_VALUE",
481
+ message: `slice "${id}" gate "${gateName}" is not one of ${[...ALLOWED_SLICE_GATE_NAMES].join(" | ")}`,
482
+ path: gatePath,
483
+ });
484
+ }
485
+ // Payload validation (status + gate-passed)
486
+ if (!gate || typeof gate !== "object" || Array.isArray(gate)) {
487
+ errors.push({
488
+ code: "E_MISSING_REQUIRED_FIELD",
489
+ message: `gate "${gateName}" must be an object with { status, gate-passed }`,
490
+ path: gatePath,
491
+ });
492
+ }
493
+ else {
494
+ const g = gate;
495
+ if (!VALID_GATE_STATUSES.includes(g.status)) {
496
+ errors.push({
497
+ code: "E_BAD_ENUM_VALUE",
498
+ message: `gate "${gateName}" status "${g.status}" is not one of ${VALID_GATE_STATUSES.join(" | ")}`,
499
+ path: `${gatePath}.status`,
500
+ });
501
+ }
502
+ if (typeof g["gate-passed"] !== "boolean") {
503
+ errors.push({
504
+ code: "E_MISSING_REQUIRED_FIELD",
505
+ message: `gate "${gateName}" gate-passed must be a boolean (got ${typeof g["gate-passed"]})`,
506
+ path: `${gatePath}.gate-passed`,
507
+ });
508
+ }
509
+ }
510
+ }
511
+ }
512
+ }
513
+ // §3.2 #4 skeleton uniqueness
514
+ if (skeletons.length > 1) {
515
+ errors.push({
516
+ code: "E_MULTIPLE_SKELETONS",
517
+ message: `at most one slice may have type: skeleton (found: ${skeletons.join(", ")})`,
518
+ path: "slice_graph.slices",
519
+ });
520
+ }
521
+ // §3.2 #3 cycle detection (Kahn's algorithm)
522
+ const cycle = detectCycle(sg.slices);
523
+ if (cycle.length > 0) {
524
+ errors.push({
525
+ code: "E_CYCLE",
526
+ message: `cycle detected involving slices: ${cycle.join(" → ")}`,
527
+ path: "slice_graph.slices",
528
+ });
529
+ }
530
+ // §3.2 #6 skeleton ancestry: every non-skeleton slice must transitively depend on the skeleton
531
+ if (skeletons.length === 1 && cycle.length === 0) {
532
+ const skeletonId = skeletons[0];
533
+ const reachable = reverseReachableFrom(skeletonId, sg.slices);
534
+ for (const id of sliceIds) {
535
+ if (id !== skeletonId && !reachable.has(id)) {
536
+ errors.push({
537
+ code: "E_ORPHAN_SLICE",
538
+ message: `slice "${id}" does not transitively depend on skeleton "${skeletonId}"`,
539
+ path: `slice_graph.slices.${id}.depends_on`,
540
+ });
541
+ }
542
+ }
543
+ }
544
+ }
545
+ /**
546
+ * v6 SCHEMA.md §3: phase_plan must be a map of string → enum (scalar) or
547
+ * { status: enum, reason?: string } (object). Mutates the manifest to
548
+ * normalize scalar values into object form so downstream code reads a
549
+ * uniform shape.
550
+ */
551
+ function validateAndNormalizePhasePlan(manifest, errors) {
552
+ const raw = manifest.phase_plan;
553
+ if (raw === undefined || raw === null)
554
+ return; // missing-required already caught
555
+ if (typeof raw !== "object" || Array.isArray(raw)) {
556
+ errors.push({
557
+ code: "E_BAD_PHASE_PLAN_SHAPE",
558
+ message: `phase_plan must be a map (got ${Array.isArray(raw) ? "array" : typeof raw})`,
559
+ path: "phase_plan",
560
+ });
561
+ return;
562
+ }
563
+ const normalized = {};
564
+ for (const [key, value] of Object.entries(raw)) {
565
+ const path = `phase_plan.${key}`;
566
+ if (typeof value === "string") {
567
+ // Scalar form: `concept: skipped`
568
+ if (!VALID_PHASE_PLAN_STATUSES.includes(value)) {
569
+ errors.push({
570
+ code: "E_BAD_PHASE_PLAN_STATUS",
571
+ message: `phase_plan.${key} status "${value}" is not one of ${VALID_PHASE_PLAN_STATUSES.join(" | ")}`,
572
+ path,
573
+ });
574
+ continue;
575
+ }
576
+ normalized[key] = { status: value };
577
+ }
578
+ else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
579
+ // Object form: `test-plan: { status: active-light, reason: "..." }`
580
+ const entry = value;
581
+ const status = entry.status;
582
+ if (typeof status !== "string") {
583
+ errors.push({
584
+ code: "E_BAD_PHASE_PLAN_SHAPE",
585
+ message: `phase_plan.${key} (object form) requires a "status" string field`,
586
+ path: `${path}.status`,
587
+ });
588
+ continue;
589
+ }
590
+ if (!VALID_PHASE_PLAN_STATUSES.includes(status)) {
591
+ errors.push({
592
+ code: "E_BAD_PHASE_PLAN_STATUS",
593
+ message: `phase_plan.${key} status "${status}" is not one of ${VALID_PHASE_PLAN_STATUSES.join(" | ")}`,
594
+ path: `${path}.status`,
595
+ });
596
+ continue;
597
+ }
598
+ const normalizedEntry = { status: status };
599
+ if (typeof entry.reason === "string") {
600
+ normalizedEntry.reason = entry.reason;
601
+ }
602
+ else if (entry.reason !== undefined) {
603
+ errors.push({
604
+ code: "E_BAD_PHASE_PLAN_SHAPE",
605
+ message: `phase_plan.${key}.reason must be a string (got ${typeof entry.reason})`,
606
+ path: `${path}.reason`,
607
+ });
608
+ }
609
+ normalized[key] = normalizedEntry;
610
+ }
611
+ else {
612
+ errors.push({
613
+ code: "E_BAD_PHASE_PLAN_SHAPE",
614
+ message: `phase_plan.${key} must be a status string or { status, reason? } object`,
615
+ path,
616
+ });
617
+ }
618
+ }
619
+ // Replace the raw map with the normalized one so downstream readers get
620
+ // uniform object-form entries.
621
+ manifest.phase_plan = normalized;
622
+ }
623
+ /**
624
+ * Advisory check: warn on phase_plan keys not in the recommended-keys table for
625
+ * this manifest's work type (v6 SCHEMA.md §3.2). Typos in keys are silent
626
+ * failures by spec (the parser does not validate against a canonical
627
+ * vocabulary), so this warning gives commands a chance to catch them at
628
+ * preflight without breaking workflows that legitimately extend the plan with
629
+ * custom keys.
630
+ *
631
+ * Implemented as a warning, not an error: SCHEMA.md §3.2 reserves promotion to
632
+ * a hard enum if real usage shows typos causing failures. Until then, command
633
+ * authors can read `result.warnings` and surface them however they want.
634
+ */
635
+ function validatePhasePlanKeysForType(manifest, warnings) {
636
+ if (!manifest.phase_plan)
637
+ return;
638
+ const allowed = RECOMMENDED_PHASE_PLAN_KEYS.get(manifest.type);
639
+ if (!allowed)
640
+ return; // unknown work type — E_BAD_ENUM_VALUE already caught that
641
+ for (const key of Object.keys(manifest.phase_plan)) {
642
+ if (!allowed.has(key)) {
643
+ warnings.push({
644
+ code: "W_UNKNOWN_PHASE_PLAN_KEY",
645
+ message: `phase_plan.${key} is not a recommended key for work type "${manifest.type}" (likely typo; SCHEMA.md §3.2 lists allowed keys)`,
646
+ path: `phase_plan.${key}`,
647
+ });
648
+ }
649
+ }
650
+ }
651
+ function validatePhaseGatePremature(manifest, errors) {
652
+ // §3.6: code-review-final cannot be passed until all slices are terminal.
653
+ // Only applies when a slice_graph exists — v6 allows manifests without one
654
+ // (slice decomposition happens at codify), and there's nothing to be
655
+ // premature about when there are no slices.
656
+ const quality = manifest.phases?.quality;
657
+ const finalGate = quality?.["code-review-final"];
658
+ if (!finalGate || !finalGate["gate-passed"])
659
+ return;
660
+ if (!manifest.slice_graph)
661
+ return;
662
+ const slices = manifest.slice_graph.slices;
663
+ const nonTerminal = Object.entries(slices).filter(([, s]) => s.status !== "complete" && s.status !== "abandoned");
664
+ if (nonTerminal.length > 0) {
665
+ errors.push({
666
+ code: "E_PHASE_GATE_PREMATURE",
667
+ message: `code-review-final.gate-passed=true but ${nonTerminal.length} slice(s) not in terminal state: ${nonTerminal.map(([id]) => id).join(", ")}`,
668
+ path: "phases.quality.code-review-final",
669
+ });
670
+ }
671
+ }
672
+ // ============================================================================
673
+ // DAG helpers
674
+ // ============================================================================
675
+ /**
676
+ * Returns the slices forming a cycle, in cycle order. Empty array if acyclic.
677
+ * Uses Kahn's algorithm: any nodes left after processing must be in cycles.
678
+ */
679
+ function detectCycle(slices) {
680
+ const indegree = new Map();
681
+ for (const id of Object.keys(slices))
682
+ indegree.set(id, 0);
683
+ for (const slice of Object.values(slices)) {
684
+ for (const dep of slice.depends_on ?? []) {
685
+ indegree.set(dep, (indegree.get(dep) ?? 0) + 1);
686
+ }
687
+ }
688
+ const queue = [];
689
+ for (const [id, deg] of indegree) {
690
+ if (deg === 0)
691
+ queue.push(id);
692
+ }
693
+ while (queue.length > 0) {
694
+ const id = queue.shift();
695
+ const slice = slices[id];
696
+ for (const dep of slice?.depends_on ?? []) {
697
+ const newDeg = (indegree.get(dep) ?? 0) - 1;
698
+ indegree.set(dep, newDeg);
699
+ if (newDeg === 0)
700
+ queue.push(dep);
701
+ }
702
+ }
703
+ const remaining = [...indegree.entries()].filter(([, deg]) => deg > 0).map(([id]) => id);
704
+ return remaining;
705
+ }
706
+ /**
707
+ * Returns the set of slice IDs that transitively depend on `target` (i.e. can
708
+ * reach target by following depends_on edges). Used for §3.2 #6 skeleton
709
+ * ancestry: every non-skeleton slice should be in reverseReachableFrom(skeleton).
710
+ */
711
+ function reverseReachableFrom(target, slices) {
712
+ const reachable = new Set();
713
+ const visit = (id) => {
714
+ if (reachable.has(id))
715
+ return;
716
+ reachable.add(id);
717
+ for (const [otherId, otherSlice] of Object.entries(slices)) {
718
+ if (otherSlice.depends_on?.includes(id) && !reachable.has(otherId)) {
719
+ visit(otherId);
720
+ }
721
+ }
722
+ };
723
+ visit(target);
724
+ reachable.delete(target); // we want descendants, not target itself
725
+ return reachable;
726
+ }
727
+ // ============================================================================
728
+ // v4 → v5 migration synthesis (SCHEMA.md §6)
729
+ // ============================================================================
730
+ function synthesizeV5FromV4(root) {
731
+ const phases = root.phases ?? {};
732
+ const build = phases.build ?? {};
733
+ const tasks = Array.isArray(build.tasks) ? build.tasks : [];
734
+ // §6 derivation rules (first match wins)
735
+ let status;
736
+ let gatePassed;
737
+ if (tasks.length === 0) {
738
+ status = "pending";
739
+ gatePassed = false;
740
+ }
741
+ else if (tasks.every((t) => t.status === "complete")) {
742
+ status = "complete";
743
+ gatePassed = true;
744
+ }
745
+ else if (tasks.some((t) => t.status === "in-progress")) {
746
+ status = "in-progress";
747
+ gatePassed = false;
748
+ }
749
+ else if (tasks.every((t) => t.status === "pending")) {
750
+ status = "pending";
751
+ gatePassed = false;
752
+ }
753
+ else {
754
+ status = "in-progress";
755
+ gatePassed = false;
756
+ }
757
+ // Strip `phases.build` from the synthesized manifest so v5 invariants apply cleanly.
758
+ const phasesWithoutBuild = {};
759
+ for (const [k, v] of Object.entries(phases)) {
760
+ if (k !== "build") {
761
+ phasesWithoutBuild[k] = v;
762
+ }
763
+ }
764
+ const synthesizedManifest = {
765
+ schema_version: "5",
766
+ name: String(root.name ?? root.feature ?? ""),
767
+ type: (root.type ?? "feature"),
768
+ description: String(root.description ?? ""),
769
+ status: (root.status ?? "in-progress"),
770
+ created: String(root.created ?? ""),
771
+ command: String(root.command ?? ""),
772
+ phases: phasesWithoutBuild,
773
+ slice_graph: {
774
+ current_slice: "legacy-build",
775
+ slices: {
776
+ "legacy-build": {
777
+ type: "feature-slice",
778
+ depends_on: [],
779
+ status,
780
+ gates: {
781
+ "build-tdd": {
782
+ status: gatePassed ? "complete" : status === "pending" ? "pending" : "in-progress",
783
+ "gate-passed": gatePassed,
784
+ },
785
+ },
786
+ },
787
+ },
788
+ },
789
+ artifacts: root.artifacts,
790
+ };
791
+ if (root.complexity)
792
+ synthesizedManifest.complexity = root.complexity;
793
+ if (root.escalated_from !== undefined)
794
+ synthesizedManifest.escalated_from = root.escalated_from;
795
+ if (root.successor_path !== undefined)
796
+ synthesizedManifest.successor_path = root.successor_path;
797
+ return synthesizedManifest;
798
+ }