@polymorphism-tech/morph-spec 8.47.0 → 9.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 (222) hide show
  1. package/CLAUDE.md +48 -39
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +37 -51
  4. package/bin/validate.js +3 -111
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-proposal.md +2 -50
  17. package/framework/commands/morph-status.md +3 -3
  18. package/framework/docs/CLI.md +585 -286
  19. package/framework/evals/review-checklist.md +82 -41
  20. package/framework/hooks/README.md +10 -11
  21. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +33 -3
  23. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  24. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  25. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  26. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  27. package/framework/hooks/claude-code/core/state-sync.js +7 -21
  28. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  29. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  30. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  31. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  32. package/framework/hooks/claude-code/statusline.py +55 -41
  33. package/framework/hooks/package.json +3 -0
  34. package/framework/hooks/shared/activity-logger.js +1 -1
  35. package/framework/hooks/shared/build-check.js +10 -6
  36. package/framework/hooks/shared/command-tokens.js +169 -0
  37. package/framework/hooks/shared/comment-rules.js +1 -1
  38. package/framework/hooks/shared/comment-scanner.js +10 -23
  39. package/framework/hooks/shared/comment-stripper.js +1 -1
  40. package/framework/hooks/shared/e2e-check.js +64 -27
  41. package/framework/hooks/shared/errata.js +72 -0
  42. package/framework/hooks/shared/feature-locator.js +1 -1
  43. package/framework/hooks/shared/frontend-locations.js +6 -0
  44. package/framework/hooks/shared/gate-question.js +18 -0
  45. package/framework/hooks/shared/phase-utils.js +15 -32
  46. package/framework/hooks/shared/slice-progress.js +118 -0
  47. package/framework/hooks/shared/spawn-command.js +23 -0
  48. package/framework/hooks/shared/state-reader.js +61 -87
  49. package/framework/hooks/shared/state-writer.js +2 -3
  50. package/framework/mcp/registry.json +3 -4
  51. package/framework/phases.json +23 -39
  52. package/framework/schemas/config.schema.json +14 -9
  53. package/framework/schemas/feature.schema.json +15 -50
  54. package/framework/skills/README.md +3 -4
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  57. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  58. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  59. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  60. package/framework/skills/core/morph-media/SKILL.md +1 -1
  61. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  62. package/framework/skills/core/morph-review/SKILL.md +111 -136
  63. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  64. package/framework/skills/core/morph-uiux/SKILL.md +77 -152
  65. package/framework/standards/STANDARDS.json +86 -1218
  66. package/framework/standards/STANDARDS.meta.json +1305 -0
  67. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  68. package/framework/standards/ai-agents/setup.md +1 -1
  69. package/framework/standards/architecture/code-comments.md +6 -6
  70. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  71. package/framework/templates/README.md +2 -3
  72. package/framework/templates/REGISTRY.json +6 -170
  73. package/framework/templates/docs/plan.md +72 -0
  74. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  75. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  76. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  77. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  78. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  80. package/package.json +1 -1
  81. package/src/commands/e2e/e2e.js +2 -2
  82. package/src/commands/observability/cost.js +18 -60
  83. package/src/commands/project/doctor-procs.js +1 -1
  84. package/src/commands/project/doctor.js +9 -9
  85. package/src/commands/project/fleet.js +4 -4
  86. package/src/commands/project/heavy.js +3 -3
  87. package/src/commands/project/init.js +4 -0
  88. package/src/commands/project/setup-infra-cmd.js +2 -1
  89. package/src/commands/project/stack-scan.js +58 -0
  90. package/src/commands/project/standards.js +79 -25
  91. package/src/commands/project/status.js +29 -69
  92. package/src/commands/project/update.js +17 -1
  93. package/src/commands/project/worktree.js +1 -1
  94. package/src/commands/state/advance.js +62 -46
  95. package/src/commands/state/approve.js +81 -141
  96. package/src/commands/state/archive.js +20 -84
  97. package/src/commands/state/auto-approval.js +68 -0
  98. package/src/commands/state/create.js +28 -1
  99. package/src/commands/state/delete.js +1 -1
  100. package/src/commands/state/finish.js +11 -23
  101. package/src/commands/state/gate-check.js +85 -84
  102. package/src/commands/state/gate-decision.js +6 -4
  103. package/src/commands/state/handoff.js +1 -1
  104. package/src/commands/state/list.js +9 -9
  105. package/src/commands/state/report-check.js +157 -311
  106. package/src/commands/state/retype.js +9 -3
  107. package/src/commands/validation/mutate.js +77 -14
  108. package/src/commands/validation/plan-check.js +81 -0
  109. package/src/commands/validation/validate-feature.js +60 -135
  110. package/src/commands/validation/verify.js +40 -72
  111. package/src/core/paths/output-schema.js +19 -66
  112. package/src/core/state/gate-order.js +5 -6
  113. package/src/core/state/lifecycle-audit.js +2 -2
  114. package/src/core/state/state-manager.js +17 -98
  115. package/src/core/templates/template-registry.js +2 -2
  116. package/src/core/templates/template-seed.js +22 -0
  117. package/src/core/work-types.js +10 -10
  118. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  119. package/src/dashboard/app/components/CommandPalette.jsx +5 -5
  120. package/src/dashboard/app/components/EntityLink.jsx +1 -1
  121. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  122. package/src/dashboard/app/lib/phases.js +2 -4
  123. package/src/dashboard/app/pages/Activity.jsx +1 -1
  124. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  125. package/src/dashboard/app/pages/Features.jsx +9 -9
  126. package/src/dashboard/app/pages/Guide.jsx +2 -2
  127. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  128. package/src/dashboard/app/pages/Standards.jsx +1 -1
  129. package/src/dashboard/scanner/activity-scanner.js +5 -15
  130. package/src/dashboard/scanner/agents-scanner.js +1 -1
  131. package/src/dashboard/scanner/docs-scanner.js +2 -2
  132. package/src/dashboard/scanner/features-scanner.js +95 -60
  133. package/src/dashboard/scanner/project-api.js +1 -1
  134. package/src/dashboard/scanner/standards-scanner.js +2 -1
  135. package/src/lib/ci/ci-verify.js +17 -1
  136. package/src/lib/ci/github-ci.js +1 -1
  137. package/src/lib/detectors/index.js +3 -8
  138. package/src/lib/generators/handoff-generator.js +34 -65
  139. package/src/lib/generators/settings-generator.js +11 -89
  140. package/src/lib/git/evidence-staleness.js +1 -1
  141. package/src/lib/git/tree-fingerprint.js +19 -7
  142. package/src/lib/git/tree-state.js +2 -2
  143. package/src/lib/heavy/cleanup.js +1 -5
  144. package/src/lib/installers/memory-installer.js +6 -8
  145. package/src/lib/mutation/commands.js +3 -2
  146. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  147. package/src/lib/mutation/plan.js +14 -6
  148. package/src/lib/mutation/runner.js +1 -1
  149. package/src/lib/mutation/subset.js +46 -9
  150. package/src/lib/plan/artifact-vocabulary.js +60 -0
  151. package/src/lib/plan/checks.js +234 -0
  152. package/src/lib/plan/plan-parser.js +209 -0
  153. package/src/lib/plan/plan-template.js +18 -0
  154. package/src/lib/review-reports.js +222 -0
  155. package/src/lib/risk-detector.js +131 -140
  156. package/src/lib/specs/adr-index.js +50 -69
  157. package/src/lib/specs/design-seed.js +18 -0
  158. package/src/lib/specs/frontmatter.js +44 -0
  159. package/src/lib/specs/gfm-table.js +17 -0
  160. package/src/lib/specs/old-format.js +68 -0
  161. package/src/lib/specs/revocations.js +37 -95
  162. package/src/lib/specs/screen-checklist.js +18 -17
  163. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  164. package/src/lib/stack/stack-scan.js +267 -0
  165. package/src/lib/standards/registry.js +43 -0
  166. package/src/lib/standards/sections.js +58 -0
  167. package/src/lib/tasks/eval-node.js +4 -12
  168. package/src/lib/tasks/test-nodes.js +1 -74
  169. package/src/lib/tasks/test-runner.js +75 -117
  170. package/src/lib/telemetry/metrics.js +8 -69
  171. package/src/lib/telemetry/telemetry-reader.js +5 -118
  172. package/src/lib/telemetry/verify-event.js +5 -7
  173. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  175. package/src/lib/validators/reuse/reuse-validator.js +22 -45
  176. package/src/lib/validators/traceability-check.js +85 -129
  177. package/src/lib/validators/verify-runner.js +118 -235
  178. package/src/lib/worktree/docker-audit.js +1 -2
  179. package/src/lib/worktree/fleet-scanner.js +25 -38
  180. package/src/scripts/setup-infra.js +9 -0
  181. package/src/utils/agents-installer.js +76 -9
  182. package/src/utils/claude-settings-manager.js +0 -35
  183. package/src/utils/file-copier.js +27 -1
  184. package/src/utils/hooks-installer.js +16 -9
  185. package/src/utils/skills-installer.js +1 -2
  186. package/framework/evals/trace-schema.md +0 -67
  187. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  188. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  189. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  190. package/framework/hooks/shared/tasks-validator.js +0 -111
  191. package/framework/rules/morph-task-tracking.md +0 -54
  192. package/framework/rules/morph-workflow.md +0 -244
  193. package/framework/schemas/tasks.schema.json +0 -79
  194. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  195. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  196. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  197. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  198. package/framework/templates/docs/proposal.md +0 -192
  199. package/framework/templates/docs/spec.md +0 -57
  200. package/framework/templates/docs/ui-components.md +0 -52
  201. package/framework/templates/docs/ui-design-system.md +0 -76
  202. package/framework/templates/docs/ui-flows.md +0 -167
  203. package/framework/templates/examples/spec-examples.md +0 -91
  204. package/framework/templates/feature/decisions.md +0 -124
  205. package/framework/templates/feature/recap.md +0 -145
  206. package/framework/templates/tasks.template.json +0 -17
  207. package/src/commands/state/usecase.js +0 -143
  208. package/src/commands/tasks/dag.js +0 -181
  209. package/src/lib/agents/output-collector.js +0 -76
  210. package/src/lib/evaluator-report.js +0 -104
  211. package/src/lib/specs/acervo-format.js +0 -70
  212. package/src/lib/specs/acervo-lifecycle.js +0 -264
  213. package/src/lib/specs/done-criteria-check.js +0 -200
  214. package/src/lib/specs/usecase-check.js +0 -301
  215. package/src/lib/specs/usecase-promoter.js +0 -301
  216. package/src/lib/specs/usecase-registry.js +0 -237
  217. package/src/lib/tasks/dispatch-planner.js +0 -319
  218. package/src/lib/tasks/mermaid-renderer.js +0 -132
  219. package/src/lib/tasks/task-classifier.js +0 -30
  220. package/src/lib/tasks/task-parser.js +0 -209
  221. package/src/lib/tasks/tasks-json-parser.js +0 -296
  222. package/src/lib/tasks/test-scope.js +0 -597
@@ -1,8 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { mkdirSync } from 'fs';
2
+ import { existsSync, mkdirSync, readFileSync, unlinkSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { getFeature, setApprovalGate, updateFeature } from '../../core/state/state-manager.js';
5
5
  import { WORK_TYPES, isValidWorkType, POLICY_APPROVER_PREFIX } from '../../core/work-types.js';
6
+ import { renderPlanSkeleton } from '../../lib/plan/plan-template.js';
6
7
 
7
8
  /**
8
9
  * `morph-spec retype <feature> <newType>` — mid-flight work-type change with
@@ -12,7 +13,7 @@ import { WORK_TYPES, isValidWorkType, POLICY_APPROVER_PREFIX } from '../../core/
12
13
  * - Blocked outright once Gate 3 (review) is approved — the feature already
13
14
  * shipped its pipeline, retyping it retroactively would be revisionist.
14
15
  * - Downgrade (fewer skipped gates → more skipped gates, e.g. feature/bug
15
- * → chore/hotfix) is blocked once Gate 2 (plan) carries a genuine
16
+ * → chore/hotfix) is blocked once the plan gate carries a genuine
16
17
  * (non-policy) approval — the plan was already accepted under the old
17
18
  * type's scrutiny; retyping around it would erase that judgment call.
18
19
  * - Upgrade (more skipped gates → fewer, e.g. chore/hotfix → feature/bug)
@@ -61,7 +62,7 @@ export async function retypeCommand(featureName, newType, options = {}) {
61
62
  const genuinelyApproved = planGate?.approved === true
62
63
  && !String(planGate.approvedBy || '').startsWith(POLICY_APPROVER_PREFIX);
63
64
  if (genuinelyApproved) {
64
- console.error(chalk.red(`\n❌ Gate 2 (plan) já aprovado por "${planGate.approvedBy}"; downgrade bloqueado — o plano já foi aceito\n`));
65
+ console.error(chalk.red(`\n❌ Gate do plano já aprovado por "${planGate.approvedBy}"; downgrade bloqueado — o plano já foi aceito\n`));
65
66
  process.exit(1);
66
67
  }
67
68
  }
@@ -93,6 +94,11 @@ export async function retypeCommand(featureName, newType, options = {}) {
93
94
  }
94
95
 
95
96
  const featureDir = join(process.cwd(), '.morph', 'features', featureName);
97
+ const planPath = join(featureDir, '2-plan', 'plan.md');
98
+ if (stampedGates.includes('plan') && existsSync(planPath)
99
+ && readFileSync(planPath, 'utf8') === renderPlanSkeleton(featureName)) {
100
+ unlinkSync(planPath);
101
+ }
96
102
  for (const folder of newWorkType.scaffold) {
97
103
  mkdirSync(join(featureDir, folder), { recursive: true });
98
104
  }
@@ -1,9 +1,9 @@
1
1
  import chalk from 'chalk';
2
2
  import { execFileSync } from 'node:child_process';
3
- import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { join, resolve } from 'node:path';
5
- import { resolveTestBuildTargets } from '../../lib/tasks/test-runner.js';
6
- import { dotnetTestMode } from '../../lib/tasks/dotnet-test-introspection.js';
5
+ import { discoverDotnetBuildTargets } from '../../lib/tasks/test-runner.js';
6
+ import { dotnetTestMode } from '../../lib/mutation/dotnet-introspection.js';
7
7
  import { dotnetBuildCommands } from '../../lib/mutation/commands.js';
8
8
  import { resolveTestNodes } from '../../lib/tasks/test-nodes.js';
9
9
  import { changedPathsAgainst, detectDefaultBranch, getHeadSha } from '../../lib/git/default-branch.js';
@@ -12,7 +12,8 @@ import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js
12
12
  import { resolveGitCommonDir } from '../../../framework/hooks/shared/git-paths.js';
13
13
  import { BACKSLASH_TOKEN, ManifestError } from '../../lib/mutation/manifest.js';
14
14
  import { groupByNode, loadPlan } from '../../lib/mutation/plan.js';
15
- import { dotnetSubset, jsSubset, testProjectDirsOf } from '../../lib/mutation/subset.js';
15
+ import { dotnetSubset, jsSubset, mutatedTypesOf, testFilesCiting, testProjectDirsOf } from '../../lib/mutation/subset.js';
16
+ import { isFinished } from '../../dashboard/scanner/feature-status.js';
16
17
  import { dotnetAdapter, genericAdapter, spawnExec } from '../../lib/mutation/adapters.js';
17
18
  import { runMutation } from '../../lib/mutation/runner.js';
18
19
  import { renderSection, summarize } from '../../lib/mutation/report.js';
@@ -97,23 +98,72 @@ export function restoreLatest(root, out = console) {
97
98
  return restored;
98
99
  }
99
100
 
101
+ function testDirsOf(node, root, { projectsOnly = false } = {}) {
102
+ const cwd = resolve(root, node.cwd || '.');
103
+ const targets = discoverDotnetBuildTargets(node, cwd);
104
+ return testProjectDirsOf(root, projectsOnly ? targets.filter((t) => !/\.slnx?$/i.test(t)) : targets, cwd);
105
+ }
106
+
100
107
  function subsetFor({ node, group, root, options }) {
101
108
  if (options.subset === false) return { usable: false, reason: '--no-subset' };
102
109
  const mutatedFiles = group.regions.map((r) => r.file);
103
110
  if (node.runner === 'dotnet') {
104
111
  const base = detectDefaultBranch(root);
105
112
  const changed = base ? changedPathsAgainst(base, root) : [];
106
- return dotnetSubset({ root, testProjectDirs: testProjectDirsOf(root, resolveTestBuildTargets(node)), mutatedFiles, changed });
113
+ return dotnetSubset({ root, testProjectDirs: testDirsOf(node, root), mutatedFiles, changed });
107
114
  }
108
115
  return jsSubset({ runner: node.runner, command: node.command, root, mutatedFiles });
109
116
  }
110
117
 
111
- export async function mutateCommand(feature, options = {}) {
118
+ function e2eManualProtocol(feature, node) {
119
+ const f = feature ?? '<feature>';
120
+ const round = `eval "$(morph-spec env ${f})" && morph-spec e2e up ${f} && morph-spec heavy run -- ${node.command}`;
121
+ return 'mutate só recompila localmente, e a suíte que alcança esse código testa o container do compose: o mutante sairia sobrevivente em falso. ' +
122
+ `Prove à mão pelo nó "${node.id}", cada passo numa linha só e encadeado por \`&&\` (nunca \`;\`: um passo que falha não pode deixar a suíte rodar sobre o código original; ` +
123
+ 'e o env da feature se perde entre chamadas de Bash): ' +
124
+ `(1) base verde antes do 1º mutante: \`${round}\`; ` +
125
+ `(2) por mutante: edite a linha && \`${round}\`; ` +
126
+ `(3) restaure, confirme com \`git diff --quiet -- <arquivo>\` e suba a stack de novo (\`eval "$(morph-spec env ${f})" && morph-spec e2e up ${f}\`). ` +
127
+ 'Registre cada mutante em 4-review (expect, resultado, teste que o matou).';
128
+ }
129
+
130
+ // A .cs file belongs to every dotnet suite, by runner, not by project reference: a unit suite
131
+ // "owns" code only an e2e suite executes. With an e2e node declared, a unit suite with no test
132
+ // naming a type of the mutated file is not evidence, so the run is refused.
133
+ function uncitedUnderE2e({ root, groups, nodes }) {
134
+ const e2e = nodes.filter((n) => n.e2e);
135
+ if (e2e.length === 0) return [];
136
+ const e2eDirs = new Set(e2e.filter((n) => n.runner === 'dotnet').flatMap((n) => testDirsOf(n, root)));
137
+ const uncited = [];
138
+ for (const { node, regions } of groups.filter((g) => g.node.runner === 'dotnet')) {
139
+ const direct = new Set(testDirsOf(node, root, { projectsOnly: true }));
140
+ const dirs = testDirsOf(node, root).filter((d) => direct.has(d) || !e2eDirs.has(d));
141
+ for (const region of regions) {
142
+ const types = mutatedTypesOf(root, [region.file]);
143
+ if (testFilesCiting({ root, testProjectDirs: dirs, types, exclude: [region.file] }).length === 0) uncited.push({ region, node, types, e2e });
144
+ }
145
+ }
146
+ return uncited;
147
+ }
148
+
149
+ function soleActiveFeature(root) {
150
+ const dir = join(root, '.morph', 'features');
151
+ let names;
152
+ try { names = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name); } catch { return null; }
153
+ const active = names.filter((name) => {
154
+ try { return !isFinished(JSON.parse(readFileSync(join(dir, name, 'feature.json'), 'utf8'))); } catch { return false; }
155
+ });
156
+ return active.length === 1 ? active[0] : null;
157
+ }
158
+
159
+ export async function mutateCommand(featureArg, options = {}) {
112
160
  const root = resolve(options.cwd ?? process.cwd());
113
161
  if (options.restore) { restoreLatest(root); return null; }
114
162
 
115
- const planPath = resolve(root, options.plan ?? join('.morph', 'features', feature ?? '', '4-review', 'mutants.json'));
116
- if (!existsSync(planPath)) return fail(`manifesto não encontrado: ${planPath}\nEscreva-o com a ferramenta Write, se você a tem; só com Bash, heredoc de delimitador entre aspas simples, com a barra invertida do código como ${BACKSLASH_TOKEN} — formato em .morph/framework/CLI.md, seção mutate.`, EXIT.usage);
163
+ const feature = featureArg ?? soleActiveFeature(root);
164
+ if (!feature && !options.plan) return fail('sem feature: passe <feature> (nenhuma, ou mais de uma, em andamento) ou o manifesto por --plan.', EXIT.usage);
165
+ const planPath = resolve(root, options.plan ?? join('.morph', 'features', feature, '4-review', 'mutants.json'));
166
+ if (!existsSync(planPath)) return fail(`manifesto não encontrado: ${planPath}\nEscreva-o com a ferramenta Write, se você a tem; só com Bash, heredoc de delimitador entre aspas simples, com a barra invertida do código como ${BACKSLASH_TOKEN} — formato em .morph/framework/CLI.md, seção "### mutate".`, EXIT.usage);
117
167
 
118
168
  let regions;
119
169
  try {
@@ -125,16 +175,26 @@ export async function mutateCommand(feature, options = {}) {
125
175
  }
126
176
 
127
177
  const { nodes } = resolveTestNodes(root);
128
- const { groups, orphans } = groupByNode(regions, nodes, root);
178
+ const { groups, orphans, stackOnly } = groupByNode(regions, nodes, root);
129
179
  if (orphans.length > 0) {
130
180
  return fail(`nenhuma suíte declarada roda: ${orphans.map((o) => `${o.id} (${o.file})`).join(', ')}\nDeclare o nó em project.tests[] (.morph/config/config.json).`, EXIT.usage);
131
181
  }
182
+ if (stackOnly.length > 0) {
183
+ const named = stackOnly.map(({ region, node }) => `${region.id} (${region.file}) → nó "${node.id}"`).join(', ');
184
+ return fail(`só uma suíte e2e: true cobre: ${named}\nProve a guarda por um teste de unidade, ou: ${e2eManualProtocol(feature, stackOnly[0].node)}`, EXIT.usage);
185
+ }
132
186
  const mtp = groups.find((g) => g.node.runner === 'dotnet' && dotnetTestMode(resolve(root, g.node.cwd || '.')) === 'mtp');
133
187
  if (mtp) return fail(`a suíte "${mtp.node.id}" roda no Microsoft.Testing.Platform (global.json): o TRX do VSTest, de onde o veredito e o motivo saem, não existe nela. Suporte a MTP ainda não há — rode a mutação com a suíte inteira à mão.`, EXIT.usage);
134
188
  const uncompilable = groups.find((g) => g.node.runner === 'dotnet' && dotnetBuildCommands(g.node, root).length === 0);
135
- if (uncompilable) return fail(`a suíte "${uncompilable.node.id}" não aponta nenhum .csproj (solution, ou nada): sem saber o que compilar, todo mutante rodaria sobre o binário antigo e sairia "sobrevivente". Declare o campo build no nó (config.json, project.tests[]).`, EXIT.usage);
189
+ if (uncompilable) return fail(`a suíte "${uncompilable.node.id}" não deixa descobrir o que compilar: o comando não nomeia .sln/.slnx/.csproj e o diretório alvo não tem uma única solution (ou um único projeto). Sem isso, todo mutante rodaria sobre o binário antigo e sairia "sobrevivente". Nomeie a solution no comando ou declare o campo build no nó (config.json, project.tests[]).`, EXIT.usage);
136
190
  const preDecided = groups.find((g) => g.node.status);
137
191
  if (preDecided) return fail(`a suíte "${preDecided.node.id}" já vem decidida como ${preDecided.node.status}: ${preDecided.node.reason ?? ''}`, EXIT.usage);
192
+ const uncited = uncitedUnderE2e({ root, groups, nodes });
193
+ if (uncited.length > 0) {
194
+ const named = uncited.map(({ region, node, types }) => `${region.id} (${region.file}; tipos: ${types.join(', ') || 'nenhum legível'}) → nenhum teste da suíte "${node.id}" os cita`).join('\n');
195
+ const e2eIds = uncited[0].e2e.map((n) => `"${n.id}"`).join(', ');
196
+ return fail(`${named}\nNó e2e: true declarado: ${e2eIds}. ${e2eManualProtocol(feature, uncited[0].e2e[0])}`, EXIT.usage);
197
+ }
138
198
 
139
199
  if (!options.allowDirty) {
140
200
  const dirty = gitOut(root, ['status', '--porcelain', '--', ...new Set(regions.map((r) => r.file))]).trim();
@@ -184,15 +244,18 @@ export async function mutateCommand(feature, options = {}) {
184
244
  const order = new Map(regions.map((r, i) => [r.id, i]));
185
245
  merged.mutants.sort((a, b) => order.get(a.id) - order.get(b.id));
186
246
  const section = renderSection(merged, { head: git.head(), date: new Date().toISOString().slice(0, 10), subset: subsets[0] ?? null });
187
- const outPath = resolve(root, options.out ?? join('.morph', 'features', feature ?? '_', '4-review', 'mutation-section.md'));
188
- mkdirSync(join(outPath, '..'), { recursive: true });
189
- writeFileSync(outPath, section);
247
+ const outRel = options.out ?? (feature ? join('.morph', 'features', feature, '4-review', 'mutation-section.md') : null);
248
+ const outPath = outRel ? resolve(root, outRel) : null;
249
+ if (outPath) {
250
+ mkdirSync(join(outPath, '..'), { recursive: true });
251
+ writeFileSync(outPath, section);
252
+ }
190
253
 
191
254
  const tally = summarize(merged);
192
255
  if (options.json) console.log(JSON.stringify({ ...merged, tally, section: outPath }, null, 2));
193
256
  else {
194
257
  console.log(section);
195
- console.log(chalk.gray(`seção gravada em ${outPath}`));
258
+ console.log(chalk.gray(outPath ? `seção gravada em ${outPath}` : 'seção não gravada: sem feature nem --out'));
196
259
  console.log(`${chalk.green(`${tally.killed} mortos`)} · ${tally.survived ? chalk.yellow(`${tally.survived} sobreviveram`) : '0 sobreviveram'} · ${tally['build-error']} não compilaram · ${tally.inconclusive} inconclusivos`);
197
260
  }
198
261
  if (!merged.restore.ok || ['mismatch', 'unlocated'].includes(merged.binary.proof) || !merged.restore.treeClean) process.exitCode = EXIT.unsafe;
@@ -0,0 +1,81 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { atomicWriteJson } from '../../core/state/state-manager.js';
5
+ import { isGateApproved } from '../../../framework/hooks/shared/state-reader.js';
6
+ import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
7
+ import { parsePlan } from '../../lib/plan/plan-parser.js';
8
+ import { buildChecks, knownPersonas, parallelGroups, planHash, validatePlan } from '../../lib/plan/checks.js';
9
+ import { knownSurfaces } from '../../lib/specs/screen-checklist.js';
10
+ import { revocationWarnings } from '../../lib/specs/adr-index.js';
11
+ import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
12
+ import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
13
+ import { reportDotnetIgnores } from '../state/advance.js';
14
+
15
+ function readJson(path) {
16
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
17
+ }
18
+
19
+ /** Parse and judge `2-plan/plan.md` of the project at `cwd`; with no error, write `2-plan/checks.json`. */
20
+ export function runPlanCheck(featureName, cwd = process.cwd()) {
21
+ const planPath = getAbsoluteOutputPath(cwd, featureName, 'plan');
22
+ const checksPath = getAbsoluteOutputPath(cwd, featureName, 'checks');
23
+ const base = { feature: featureName, plan: planPath, checksPath };
24
+ if (!existsSync(planPath)) return { ...base, ok: false, exit: 2, error: 'plan-not-found', errors: [], warnings: [], groups: [] };
25
+
26
+ const text = readFileSync(planPath, 'utf8');
27
+ const plan = parsePlan(text);
28
+ const { errors, warnings } = validatePlan(plan, { personas: knownPersonas(cwd), surfaces: knownSurfaces(cwd) });
29
+ warnings.push(...revocationWarnings(cwd, plan));
30
+ const groups = errors.length === 0 ? parallelGroups(plan.slices) : [];
31
+ const hash = planHash(text);
32
+ const result = { ...base, planHash: hash, errors, warnings, groups, written: false };
33
+ if (errors.length > 0) return { ...result, ok: false, exit: 1 };
34
+
35
+ const approved = isGateApproved(featureName, 'plan', cwd);
36
+ const previous = existsSync(checksPath) ? readJson(checksPath) : null;
37
+ if (approved && previous && previous.planHash !== hash) {
38
+ return { ...result, ok: false, exit: 1, error: 'frozen', previousHash: previous.planHash ?? null };
39
+ }
40
+
41
+ atomicWriteJson(checksPath, buildChecks(featureName, text, plan));
42
+ return { ...result, ok: true, exit: 0, written: true };
43
+ }
44
+
45
+ export function planCheckCommand(featureName, options = {}) {
46
+ const owner = assertOwnerTree(featureName, process.cwd());
47
+ if (owner.blocked) {
48
+ for (const { tone, text } of ownerTreeRefusalLines(featureName, owner.tree, owner.branch, `morph-spec plan-check ${featureName}`)) {
49
+ console.error(chalk[tone](text));
50
+ }
51
+ process.exit(1);
52
+ }
53
+
54
+ const result = runPlanCheck(featureName);
55
+ const gitignoreAdded = result.written ? seedDotnetBuildIgnores(process.cwd(), { featureName }) : [];
56
+ if (options.json) {
57
+ if (gitignoreAdded.length) result.gitignoreAdded = gitignoreAdded;
58
+ console.log(JSON.stringify(result, null, 2));
59
+ process.exit(result.exit);
60
+ }
61
+
62
+ if (result.error === 'plan-not-found') {
63
+ console.error(chalk.red(`plan-check: ${result.plan} não existe — o create escreve o esqueleto em 2-plan/plan.md (chore/hotfix: você o cria); gramática completa: morph-spec plan-check --help`));
64
+ process.exit(2);
65
+ }
66
+ for (const e of result.errors) console.log(chalk.red(` ✗ ${e}`));
67
+ for (const w of result.warnings) console.log(chalk.yellow(` ⚠ ${w}`));
68
+
69
+ if (result.error === 'frozen') {
70
+ console.log(chalk.red(`✗ plan-check: o gate plan de ${featureName} está aprovado e o plan.md mudou fora da errata — checks.json congelado.`));
71
+ console.log(chalk.gray(' Fato errado sobre o código: acrescente em ## Errata (não muda o planHash).'));
72
+ console.log(chalk.gray(` Mudança real de escopo: morph-spec unapprove ${featureName} plan, rode plan-check e re-apresente o gate.`));
73
+ } else if (!result.ok) {
74
+ console.log(chalk.red(`✗ plan-check: ${result.errors.length} erro(s) em ${result.plan} — checks.json não foi gravado`));
75
+ } else {
76
+ console.log(chalk.green(`✓ plan-check: ${join('2-plan', 'checks.json')} gravado (${result.planHash.slice(0, 12)})`));
77
+ result.groups.forEach((g, i) => console.log(chalk.gray(` onda ${i + 1}: ${g.join(', ')}${g.length > 1 ? ' (paralelo)' : ''}`)));
78
+ reportDotnetIgnores(gitignoreAdded);
79
+ }
80
+ process.exit(result.exit);
81
+ }
@@ -15,95 +15,43 @@
15
15
  import fs from 'fs';
16
16
  import path from 'path';
17
17
  import chalk from 'chalk';
18
- import { validateTasksJson } from '../../lib/tasks/tasks-json-parser.js';
18
+ import { parsePlan } from '../../lib/plan/plan-parser.js';
19
+ import { knownPersonas, planHash, validatePlan } from '../../lib/plan/checks.js';
19
20
 
20
21
  /**
21
- * Expected sections per output file — realigned with what the core skills
22
- * actually emit (`morph-brainstorming`, `morph-plan`, `morph-review`) so that
23
- * the framework's own output passes its own validator.
22
+ * Expected sections per markdown output — matched against *section markers*
23
+ * (headings and line-start bold labels), not whole-file substrings; see
24
+ * `extractSectionMarkers`. plan.md is judged by the plan parser instead.
24
25
  *
25
- * Three tiers:
26
- * required → ERROR if absent. Strict subset of what the skills always emit.
27
- * recommended → WARNING if absent. Skills emit these, but a lean doc may skip.
26
+ * required → ERROR if absent.
27
+ * recommended → WARNING if absent.
28
28
  * optional → silent. Tracked when present, never penalized.
29
29
  *
30
- * Each entry is a string or an array of acceptable strings (any-of). The array
31
- * form carries bilingual aliases (pt-BR section names the skills emit AND their
32
- * English equivalents in the canonical templates), e.g. ['Escopo', 'Scope'].
33
- *
34
- * Matching is done against *section markers* (headings and line-start bold
35
- * labels), not whole-file substrings — see `extractSectionMarkers`.
30
+ * Each entry is a string or an array of acceptable aliases (any-of).
36
31
  */
37
32
  const OUTPUT_SECTIONS = {
38
- // morph-brainstorming emits: Problema, Escopo, Usuários e fluxos críticos,
39
- // Stack, Riscos e dependências, Compliance de stack, Conflitos com o acervo.
40
- //
41
- // "Critérios de aceite" is now OPTIONAL, not required: the behaviour layer
42
- // moved to the use cases (2-plan/usecases/), whose post-conditions must be
43
- // observable in a test, and morph-brainstorming no longer emits the section.
44
- // `optional` — not `recommended` — precisely because the skill never emits it
45
- // anymore: a warning on every new proposal would be systematic noise. Legacy
46
- // proposals that DO carry the section keep validating exactly as before.
47
- //
48
- // "Conflitos com o acervo" is recommended: morph-brainstorming always emits
49
- // it ("nenhum" included), so its absence means the acervo check never ran.
50
- // Not required — proposals written before the section existed stay valid.
51
- 'proposal.md': {
52
- required: [
53
- ['Problema', 'Problem'],
54
- ['Escopo', 'Scope'],
55
- ],
56
- recommended: [
57
- ['Riscos', 'Risks'],
58
- ['Conflitos', 'Conflicts'],
59
- ],
60
- optional: [
61
- ['Critérios de aceite', 'Critério', 'Aceite', 'Acceptance Criteria', 'Acceptance', 'Success Criteria', 'Success Metrics'],
62
- 'Stack',
63
- ['Usuários', 'Usuarios', 'Users', 'User Journey', 'User Stories', 'Fluxos'],
64
- ['Dependências', 'Dependencias', 'Dependencies'],
65
- ['Compliance'],
66
- ['Overview', 'Visão geral', 'Visao geral', 'Resumo'],
67
- ],
68
- },
69
- // morph-plan (spec.md) emits: Arquitetura da solução, Edge cases,
70
- // Integrações externas e contratos esperados.
71
- 'spec.md': {
33
+ 'verifier.md': {
72
34
  required: [
73
- ['Arquitetura', 'Architecture'],
74
- ['Integrações', 'Integracoes', 'Integrations', 'Contratos', 'Contracts', 'Data Model', 'Modelo de Dados'],
35
+ ['Verdict', 'Veredito'],
75
36
  ],
76
37
  recommended: [
77
- ['Edge cases', 'Edge Cases', 'Casos limite', 'Casos Limite', 'Casos de borda'],
78
- ],
79
- optional: [
80
- ['Overview', 'Visão geral', 'Resumo'],
81
- ['Requirements', 'Requisitos'],
82
- ['Security', 'Segurança', 'Seguranca'],
83
- ['Testing', 'Testes', 'Estratégia de Testes'],
84
- ['Performance', 'Desempenho'],
38
+ ['Bloqueadores', 'Bloqueantes', 'Blockers'],
39
+ 'Checks',
85
40
  ],
41
+ optional: [],
86
42
  },
87
- // decisions.md follows the ADR convention: an `### ADR-NNN` heading with
88
- // `**Status:** / **Context:** / **Decision:**` line-start bold labels.
89
- 'decisions.md': {
43
+ 'reviewer.md': {
90
44
  required: [
91
- 'ADR',
92
- 'Status',
93
- ['Contexto', 'Context'],
94
- ['Decisão', 'Decisao', 'Decision'],
45
+ ['Verdict', 'Veredito'],
95
46
  ],
96
47
  recommended: [
97
- ['Consequências', 'Consequencias', 'Consequences', 'Implicações', 'Trade-offs', 'Trade-off'],
98
- ],
99
- optional: [
100
- ['Alternativas', 'Alternatives'],
101
- ['Custos', 'Costs'],
48
+ ['Bloqueadores', 'Bloqueantes', 'Blockers'],
102
49
  ],
50
+ optional: [],
103
51
  },
104
- 'review-report.md': {
52
+ 'security.md': {
105
53
  required: [
106
- ['Veredito', 'Verdict'],
54
+ ['Verdict', 'Veredito'],
107
55
  ],
108
56
  recommended: [
109
57
  ['Bloqueadores', 'Bloqueantes', 'Blockers'],
@@ -116,28 +64,18 @@ const OUTPUT_SECTIONS = {
116
64
  * Phase-to-output mapping (v5 NLH)
117
65
  */
118
66
  const PHASE_OUTPUTS = {
119
- proposal: ['proposal.md'],
67
+ plan: ['plan.md', 'checks.json'],
120
68
  uiux: ['design-system.md', 'mockups.md', 'components.md', 'flows.md'],
121
- plan: ['spec.md', 'tasks.json'],
122
- implement: ['recap.md'],
123
- review: ['review-report.md'],
69
+ implement: [],
70
+ review: ['verifier.md', 'reviewer.md'],
124
71
  };
125
72
 
126
- // Work types whose plan phase (see src/core/work-types.js) skips proposal.md/
127
- // spec.md — chore and hotfix stamp proposal+plan gates at birth
128
- // (policy-approved, no human deliberation over those docs) and go straight to
129
- // tasks.json. Validating the full plan doc set against a feature that was
130
- // never asked to write those docs would be a false failure, not a real gap.
131
- const LEAN_PLAN_WORK_TYPES = ['chore', 'hotfix'];
132
- const LEAN_PLAN_OUTPUTS = ['tasks.json'];
133
-
134
73
  /**
135
74
  * Phase-to-folder mapping (v5 filesystem convention)
136
75
  */
137
76
  const PHASE_FOLDERS = {
138
- proposal: '0-proposal',
139
- uiux: '1-design',
140
77
  plan: '2-plan',
78
+ uiux: '1-design',
141
79
  implement: '3-implement',
142
80
  review: '4-review',
143
81
  };
@@ -149,11 +87,10 @@ const PHASE_FOLDERS = {
149
87
  * - a markdown heading → `## Arquitetura da solução`
150
88
  * - a line-start bold label → `**Status:** Aceito`
151
89
  *
152
- * Bold labels are included on purpose (deviation from heading-only): the ADR
153
- * convention used in `decisions.md` — and in the spec.md ADR block — writes
154
- * Status/Context/Decision as line-start bold labels, not headings. They are
155
- * structural, not prose, so this keeps ADRs valid without reintroducing the
156
- * accidental-prose-match problem that whole-file substring matching had.
90
+ * Bold labels are included on purpose (deviation from heading-only): reports
91
+ * write `**Verdict:**` as a line-start bold label, not a heading. They are
92
+ * structural, not prose, so this avoids the accidental-prose-match problem that
93
+ * whole-file substring matching had.
157
94
  *
158
95
  * Returns lowercased marker texts.
159
96
  */
@@ -197,9 +134,12 @@ function validateOutput(featurePath, outputFile) {
197
134
  const content = fs.readFileSync(filePath, 'utf8');
198
135
  result.lineCount = content.split('\n').length;
199
136
 
200
- // tasks.json: structural validation against the schema, not markdown sections.
201
- if (outputFile === 'tasks.json') {
202
- validateTasksJsonOutput(content, result);
137
+ if (outputFile === 'plan.md') {
138
+ validatePlanOutput(content, featurePath, result);
139
+ return result;
140
+ }
141
+ if (outputFile === 'checks.json') {
142
+ validateChecksOutput(content, featurePath, result);
203
143
  return result;
204
144
  }
205
145
 
@@ -209,7 +149,6 @@ function validateOutput(featurePath, outputFile) {
209
149
  return result;
210
150
  }
211
151
 
212
- // v5: only markdown outputs — contracts moved into spec.md
213
152
  validateMarkdownSections(content, sections, result);
214
153
 
215
154
  // Size warnings
@@ -221,27 +160,33 @@ function validateOutput(featurePath, outputFile) {
221
160
  }
222
161
 
223
162
  /**
224
- * Validate tasks.json structurally: parseable JSON + conforms to the schema +
225
- * has at least one task.
163
+ * plan.md is judged by the same parser and rules as `morph-spec plan-check`.
164
+ * `planDir` is `.morph/features/{f}/2-plan`, so the project is four levels up.
226
165
  */
227
- function validateTasksJsonOutput(content, result) {
228
- let data;
166
+ function validatePlanOutput(content, planDir, result) {
167
+ const plan = parsePlan(content);
168
+ const { errors, warnings } = validatePlan(plan, { personas: knownPersonas(path.resolve(planDir, '..', '..', '..', '..')) });
169
+ result.errors.push(...errors);
170
+ result.warnings.push(...warnings);
171
+ result.sections.found.push(...plan.sections.filter((x) => x.key).map((x) => x.name));
172
+ }
173
+
174
+ /** checks.json must parse and carry the hash of the plan.md beside it. */
175
+ function validateChecksOutput(content, planDir, result) {
176
+ let doc;
229
177
  try {
230
- data = JSON.parse(content);
178
+ doc = JSON.parse(content);
231
179
  } catch (err) {
232
- result.errors.push(`tasks.json is not valid JSON: ${err.message}`);
180
+ result.errors.push(`checks.json is not valid JSON: ${err.message}`);
233
181
  return;
234
182
  }
235
- const { valid, errors } = validateTasksJson(data);
236
- if (!valid) {
237
- for (const e of errors) result.errors.push(e);
183
+ const planPath = path.join(planDir, 'plan.md');
184
+ if (!fs.existsSync(planPath)) return;
185
+ if (doc?.planHash !== planHash(fs.readFileSync(planPath, 'utf8'))) {
186
+ result.errors.push('checks.json is stale — plan.md changed after it was generated; run `morph-spec plan-check`');
238
187
  return;
239
188
  }
240
- if (data.tasks.length === 0) {
241
- result.errors.push('tasks.json has no task entries');
242
- return;
243
- }
244
- result.sections.found.push(`${data.tasks.length} task(s)`);
189
+ result.sections.found.push(`${doc.checks?.length ?? 0} check(s), ${doc.slices?.length ?? 0} slice(s)`);
245
190
  }
246
191
 
247
192
  /**
@@ -294,16 +239,11 @@ function validateMarkdownSections(content, sections, result) {
294
239
  *
295
240
  * @param {string} featurePath Absolute path to the feature directory.
296
241
  * @param {string|null} phase Phase key, or null to validate everything.
297
- * @param {string|undefined} workType Feature's workType (see src/core/work-types.js).
298
- * When `phase === 'plan'` and workType is 'chore'/'hotfix', only tasks.json
299
- * is required (see LEAN_PLAN_WORK_TYPES above).
300
242
  * @returns {{ outputFiles?: Array<{file:string, dir:string}>, error?: string }}
301
243
  */
302
- function resolveOutputFiles(featurePath, phase, workType) {
244
+ function resolveOutputFiles(featurePath, phase) {
303
245
  if (phase) {
304
- const files = (phase === 'plan' && LEAN_PLAN_WORK_TYPES.includes(workType))
305
- ? LEAN_PLAN_OUTPUTS
306
- : PHASE_OUTPUTS[phase];
246
+ const files = PHASE_OUTPUTS[phase];
307
247
  if (!files) return { error: `Unknown phase: ${phase}` };
308
248
  const phaseFolder = PHASE_FOLDERS[phase];
309
249
  const searchPath = phaseFolder ? path.join(featurePath, phaseFolder) : featurePath;
@@ -312,7 +252,7 @@ function resolveOutputFiles(featurePath, phase, workType) {
312
252
 
313
253
  // Validate every existing output across the feature root + phase folders.
314
254
  const outputFiles = [];
315
- const isOutput = name => name.endsWith('.md') || name.endsWith('.cs') || name === 'tasks.json';
255
+ const isOutput = name => name.endsWith('.md') || name.endsWith('.cs') || name === 'checks.json';
316
256
  const entries = fs.readdirSync(featurePath, { withFileTypes: true });
317
257
  for (const entry of entries) {
318
258
  if (entry.isDirectory()) {
@@ -331,7 +271,7 @@ function resolveOutputFiles(featurePath, phase, workType) {
331
271
  * Pure validation core — no console output, no process.exit.
332
272
  *
333
273
  * @param {string} featurePath Absolute path to the feature directory.
334
- * @param {{ phase?: string, workType?: string }} [options]
274
+ * @param {{ phase?: string }} [options]
335
275
  * @returns {{
336
276
  * feature: string, phase: string, valid: boolean, error?: string,
337
277
  * totalErrors: number, totalWarnings: number, totalFound: number,
@@ -340,10 +280,9 @@ function resolveOutputFiles(featurePath, phase, workType) {
340
280
  */
341
281
  export function runFeatureValidation(featurePath, options = {}) {
342
282
  const phase = options.phase || null;
343
- const workType = options.workType || null;
344
283
  const feature = path.basename(featurePath);
345
284
 
346
- const { outputFiles, error } = resolveOutputFiles(featurePath, phase, workType);
285
+ const { outputFiles, error } = resolveOutputFiles(featurePath, phase);
347
286
  if (error) {
348
287
  return {
349
288
  feature,
@@ -397,23 +336,9 @@ export async function validateFeatureCommand(featureName, options = {}) {
397
336
  process.exit(1);
398
337
  }
399
338
 
400
- // workType is read straight off the feature's own feature.json (not via
401
- // state-manager's getFeature/loadState) so a broken/missing root state.json
402
- // elsewhere in the project can never make `validate` itself throw — it just
403
- // falls back to the full (non-lean) output set, same as today.
404
- let workType;
405
- try {
406
- const fjPath = path.join(featurePath, 'feature.json');
407
- if (fs.existsSync(fjPath)) {
408
- workType = JSON.parse(fs.readFileSync(fjPath, 'utf8')).workType;
409
- }
410
- } catch {
411
- workType = undefined;
412
- }
413
-
414
- const validation = runFeatureValidation(featurePath, { phase, workType });
339
+ const validation = runFeatureValidation(featurePath, { phase });
415
340
  const { results, totalErrors, totalWarnings, totalFound } = validation;
416
- const schemaWarnings = []; // v5: cross-validation removed (contracts in spec.md)
341
+ const schemaWarnings = [];
417
342
 
418
343
  // JSON output mode
419
344
  if (options.json) {