@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,30 +0,0 @@
1
- /**
2
- * Task classifier — identifies test/validation and infrastructure tasks by title.
3
- */
4
-
5
- const TEST_TASK_PATTERN = /\b(tests?|testing|specs?|coverage|e2e|unit|integration|integra[cç][aã]o|valida[cç][aã]o|cobertura|testes?)\b/i;
6
-
7
- /**
8
- * Returns true if the task title indicates a testing or validation task.
9
- *
10
- * @param {string} taskTitle
11
- * @returns {boolean}
12
- */
13
- export function isTestTask(taskTitle) {
14
- if (!taskTitle) return false;
15
- return TEST_TASK_PATTERN.test(taskTitle);
16
- }
17
-
18
- const INFRA_TASK_PATTERN = /\b(infrastructure|infra|cleanup|migration|migra[cç][aã]o|config|setup|ci\/cd|devops|deploy|docker|packages?|dependencies|tags)\b/i;
19
-
20
- /**
21
- * Returns true if the task title indicates an infrastructure/cleanup/migration task.
22
- * Used to skip contract validation and suppress breaking change warnings.
23
- *
24
- * @param {string} taskTitle
25
- * @returns {boolean}
26
- */
27
- export function isInfrastructureTask(taskTitle) {
28
- if (!taskTitle) return false;
29
- return INFRA_TASK_PATTERN.test(taskTitle);
30
- }
@@ -1,209 +0,0 @@
1
- /**
2
- * Task Parser — Shared utilities for reading, parsing, and grouping tasks.
3
- *
4
- * Extracted from bin/task-manager.cjs and dispatch-agents.js so all
5
- * consumers share the same parsing logic without duplication.
6
- */
7
-
8
- import { readFile } from 'fs/promises';
9
- import { readFileSync, existsSync } from 'fs';
10
- import { join } from 'path';
11
-
12
- // ============================================================================
13
- // TASK_CATEGORY_TO_GROUP — maps task categories to parallel groups
14
- // ============================================================================
15
-
16
- const TASK_CATEGORY_TO_GROUP = {
17
- 'domain': 'backend',
18
- 'domain-bc': 'backend',
19
- 'infrastructure': 'backend',
20
- 'application': 'backend',
21
- 'presentation': 'frontend',
22
- 'tests': 'tests',
23
- 'infra': 'infra',
24
- 'docs': 'docs',
25
- };
26
-
27
- // ============================================================================
28
- // parseTaskList — structured task parsing with error reporting
29
- // ============================================================================
30
-
31
- /**
32
- * Parse tasks.md content into structured task objects with error reporting.
33
- *
34
- * @param {string} content - Raw tasks.md content
35
- * @returns {{ tasks: ParsedTask[], errors: TaskParseError[], groups: Object<string, string[]> }}
36
- */
37
- export function parseTaskList(content) {
38
- const tasks = [];
39
- const errors = [];
40
- const groups = {};
41
-
42
- const taskHeadingRe = /^###\s+(T\d{3,})\s*[—–:\-]\s*(.+)$/gm;
43
- const groupRe = /(?:\*\*Group\*\*\s*:\s*`?([a-z-]+)`?|- \*\*Group:\*\*\s*`?([a-z-]+)`?)/i;
44
- const categoryRe = /(?:\*\*category\*\*\s*:\s*`?([a-z-]+)`?|category:\s*([a-z-]+))/i;
45
-
46
- let headingMatch;
47
- const taskPositions = [];
48
-
49
- while ((headingMatch = taskHeadingRe.exec(content)) !== null) {
50
- taskPositions.push({ id: headingMatch[1], title: headingMatch[2].trim(), pos: headingMatch.index });
51
- }
52
-
53
- for (let i = 0; i < taskPositions.length; i++) {
54
- const { id, title, pos } = taskPositions[i];
55
- const end = i + 1 < taskPositions.length ? taskPositions[i + 1].pos : content.length;
56
- const block = content.slice(pos, end);
57
-
58
- // Try group field first (v4), fall back to category field (v3)
59
- const grpMatch = groupRe.exec(block);
60
- const catMatch = categoryRe.exec(block);
61
- let group;
62
- if (grpMatch) {
63
- group = (grpMatch[1] || grpMatch[2]).toLowerCase();
64
- } else if (catMatch) {
65
- const category = (catMatch[1] || catMatch[2]).toLowerCase();
66
- group = TASK_CATEGORY_TO_GROUP[category] || 'backend';
67
- } else {
68
- group = 'backend';
69
- }
70
-
71
- tasks.push({ id, title, group });
72
-
73
- if (!groups[group]) groups[group] = [];
74
- groups[group].push(id);
75
- }
76
-
77
- return { tasks, errors, groups };
78
- }
79
-
80
- // ============================================================================
81
- // parseAndGroupTasks — drop-in replacement for dispatch-agents.js
82
- // ============================================================================
83
-
84
- /**
85
- * Parse tasks.md and return a map of { group -> taskId[] }.
86
- * Gracefully returns {} if the file doesn't exist or is unparseable.
87
- *
88
- * @param {string} tasksPath - Absolute path to tasks.md
89
- * @returns {Object<string, string[]>}
90
- */
91
- export function parseAndGroupTasks(tasksPath) {
92
- if (!existsSync(tasksPath)) return {};
93
-
94
- try {
95
- const content = readFileSync(tasksPath, 'utf8');
96
- return parseTaskList(content).groups;
97
- } catch {
98
- return {};
99
- }
100
- }
101
-
102
- // ============================================================================
103
- // parseTasksMd
104
- // ============================================================================
105
-
106
- /**
107
- * Parse tasks.md to extract task stubs for the v3 state format.
108
- * Accepts headings in either format:
109
- * ### T001 — Task title (em-dash / en-dash / hyphen)
110
- * ### T001: Task title (colon)
111
- *
112
- * @param {string} featureName
113
- * @param {string} [cwd] - Project root (defaults to process.cwd())
114
- * @returns {Promise<Array<{id, title, status, dependencies, files, checkpoint}>>}
115
- */
116
- export async function parseTasksMd(featureName, cwd = process.cwd()) {
117
- const tasksPath = join(cwd, `.morph/features/${featureName}/2-plan/tasks.md`);
118
- let content = '';
119
- try {
120
- content = await readFile(tasksPath, 'utf-8');
121
- } catch {
122
- return [];
123
- }
124
-
125
- // Phase 1: collect heading positions
126
- const headingRe = /^###\s+(T\d+)\s*[—–:\-]\s*(.+)$/gm;
127
- const depRe = /\*\*Dependenc(?:y|ies):\*\*\s*(.+)/i;
128
- const positions = [];
129
- let match;
130
- while ((match = headingRe.exec(content)) !== null) {
131
- positions.push({ id: match[1], title: match[2].trim(), pos: match.index });
132
- }
133
-
134
- // Phase 2: extract body and parse dependencies
135
- const tasks = [];
136
- for (let i = 0; i < positions.length; i++) {
137
- const { id, title, pos } = positions[i];
138
- const end = i + 1 < positions.length ? positions[i + 1].pos : content.length;
139
- const block = content.slice(pos, end);
140
-
141
- let dependencies = [];
142
- const depMatch = depRe.exec(block);
143
- if (depMatch) {
144
- const val = depMatch[1].trim();
145
- if (val.toLowerCase() !== 'none') {
146
- dependencies = val.split(/[,\s]+/).filter(s => /^T\d+$/.test(s));
147
- }
148
- }
149
-
150
- tasks.push({ id, title, status: 'pending', dependencies, files: [], checkpoint: null });
151
- }
152
- return tasks;
153
- }
154
-
155
- // ============================================================================
156
- // ensureTaskList
157
- // ============================================================================
158
-
159
- /**
160
- * Ensure feature.taskList exists (array of individual task objects).
161
- *
162
- * In v3 state, feature.tasks is a counter object {total, completed, …}.
163
- * Individual task objects live in feature.taskList.
164
- *
165
- * @param {Object} feature - Feature state object (mutated in place)
166
- * @param {string} featureName
167
- * @param {string} [cwd]
168
- * @returns {Promise<Array>} The task list
169
- */
170
- export async function ensureTaskList(feature, featureName, cwd = process.cwd()) {
171
- if (Array.isArray(feature.tasks)) {
172
- // v2 format: tasks IS the array
173
- return feature.tasks;
174
- }
175
- // v3 format: use taskList or build from tasks.md
176
- if (!feature.taskList || feature.taskList.length === 0) {
177
- feature.taskList = await parseTasksMd(featureName, cwd);
178
- }
179
- return feature.taskList;
180
- }
181
-
182
- // ============================================================================
183
- // syncCounters
184
- // ============================================================================
185
-
186
- /**
187
- * After modifying taskList, sync the counter fields back to feature.tasks.
188
- * tasks.total is derived from the actual taskList length (not trusted from state).
189
- *
190
- * @deprecated Prefer reading directly from feature.taskList. Counter fields
191
- * (feature.tasks) duplicate native Claude Code task tracking. This function
192
- * is kept for backward compatibility but will be removed in a future version.
193
- *
194
- * @param {Object} feature - Feature state object (mutated in place)
195
- */
196
- export function syncCounters(feature) {
197
- if (Array.isArray(feature.tasks)) return; // v2 format — nothing to sync
198
- // Safety: ensure feature.tasks is an object (may be undefined after migration or corruption)
199
- if (!feature.tasks || typeof feature.tasks !== 'object') {
200
- feature.tasks = { total: 0, completed: 0, inProgress: 0, pending: 0 };
201
- }
202
- const list = feature.taskList || [];
203
- // Exclude expanded tasks — they are replaced by their sub-tasks
204
- const active = list.filter(t => t.status !== 'expanded');
205
- feature.tasks.total = active.length;
206
- feature.tasks.completed = active.filter(t => t.status === 'completed').length;
207
- feature.tasks.inProgress = active.filter(t => t.status === 'in_progress').length;
208
- feature.tasks.pending = active.filter(t => t.status === 'pending').length;
209
- }
@@ -1,296 +0,0 @@
1
- /**
2
- * tasks-json-parser.js — read, validate, query, mutate tasks.json
3
- *
4
- * Replaces the prior Markdown-based tasks.md parser for new features.
5
- * Legacy features that still use tasks.md are handled by task-parser.js.
6
- *
7
- * Uses a minimal hand-rolled JSON Schema validator (no AJV dep) — sufficient
8
- * for the controlled schema in framework/schemas/tasks.schema.json.
9
- */
10
-
11
- import fs from 'fs-extra';
12
- import { readFileSync, existsSync } from 'fs';
13
- import { join, dirname } from 'path';
14
- import { fileURLToPath } from 'url';
15
- import { UC_ID_RE } from '../specs/acervo-format.js';
16
-
17
- const __dirname = dirname(fileURLToPath(import.meta.url));
18
- const SCHEMA_PATH = join(__dirname, '..', '..', '..', 'framework', 'schemas', 'tasks.schema.json');
19
-
20
- let cachedSchema = null;
21
- export function getSchema() {
22
- if (cachedSchema) return cachedSchema;
23
- cachedSchema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
24
- return cachedSchema;
25
- }
26
-
27
- const VALID_STATUS = ['pending', 'in_progress', 'done', 'skipped', 'blocked'];
28
- const VALID_EFFORT = ['S', 'M', 'L', 'XL'];
29
- const VALID_GROUP = ['backend', 'frontend', 'tests', 'infra', 'docs'];
30
- /** Veredito de reuso por conceito — ver framework/schemas/tasks.schema.json. */
31
- const VALID_REUSE_VERDICT = ['REUSAR', 'ESTENDER', 'CRIAR'];
32
-
33
- /**
34
- * Validate a tasks.json structure.
35
- * @returns {{ valid: boolean, errors: string[] }}
36
- */
37
- export function validateTasksJson(data) {
38
- const errors = [];
39
- if (!data || typeof data !== 'object') {
40
- errors.push('tasks.json must be an object');
41
- return { valid: false, errors };
42
- }
43
- if (data.version !== '1.0') errors.push('version must be "1.0"');
44
- if (typeof data.feature !== 'string' || !data.feature) {
45
- errors.push('feature must be a non-empty string');
46
- }
47
- if (!Array.isArray(data.tasks)) {
48
- errors.push('tasks must be an array');
49
- return { valid: false, errors };
50
- }
51
-
52
- const ids = new Set();
53
- for (const [i, task] of data.tasks.entries()) {
54
- const ctx = `tasks[${i}]`;
55
- if (!task.id || !/^T\d+$/.test(task.id)) {
56
- errors.push(`${ctx}.id must match /^T\\d+$/`);
57
- } else if (ids.has(task.id)) {
58
- errors.push(`${ctx}.id duplicate: ${task.id}`);
59
- }
60
- ids.add(task.id);
61
-
62
- if (typeof task.title !== 'string' || !task.title) errors.push(`${ctx}.title required`);
63
- if (typeof task.description !== 'string' || !task.description) errors.push(`${ctx}.description required`);
64
- if (!Array.isArray(task.dependencies)) {
65
- errors.push(`${ctx}.dependencies must be array`);
66
- } else {
67
- for (const dep of task.dependencies) {
68
- if (typeof dep !== 'string' || !/^T\d+$/.test(dep)) {
69
- errors.push(`${ctx}.dependencies[${dep}] must match /^T\\d+$/`);
70
- }
71
- }
72
- }
73
- if (!VALID_EFFORT.includes(task.effort)) {
74
- errors.push(`${ctx}.effort must be one of ${VALID_EFFORT.join(', ')}`);
75
- }
76
- if (typeof task.doneCriteria !== 'string' || !task.doneCriteria) {
77
- errors.push(`${ctx}.doneCriteria required`);
78
- }
79
- if (!VALID_STATUS.includes(task.status)) {
80
- errors.push(`${ctx}.status must be one of ${VALID_STATUS.join(', ')}`);
81
- }
82
- if (task.group !== undefined && !VALID_GROUP.includes(task.group)) {
83
- errors.push(`${ctx}.group must be one of ${VALID_GROUP.join(', ')}`);
84
- }
85
- // Optional: a task with no observable behaviour references no use case.
86
- // When present it must be the bare ID — `morph-spec dag` is the gate that
87
- // catches "UC-07-listar.md" or "UC7" before the plan reaches Gate 2.
88
- if (task.usecase !== undefined && (typeof task.usecase !== 'string' || !UC_ID_RE.test(task.usecase))) {
89
- errors.push(`${ctx}.usecase must match /^UC-\\d+$/ (the ID alone, e.g. "UC-07")`);
90
- }
91
-
92
- // Optional: morph-plan may stamp a persona + standards per task (R2 token
93
- // cut — `morph-spec standards --find` replaces reading the whole
94
- // STANDARDS.json). Both are part of the plan contract and frozen after
95
- // Gate 2 by protect-spec-files (any key outside MUTABLE_SPEC_FIELDS is
96
- // blocked by construction — see that hook's top comment). Type-check
97
- // only here: no cross-check against agents.json, since stack-filtered
98
- // installs carry fewer personas than the full set and would false-fail.
99
- if (task.agent !== undefined) {
100
- if (typeof task.agent !== 'string' || !task.agent.trim()) {
101
- errors.push(`${ctx}.agent must be a non-empty string (persona id, e.g. "dotnet-senior")`);
102
- } else if (task.agent.startsWith('morph-')) {
103
- errors.push(`${ctx}.agent must be the bare persona id — no "morph-" prefix (got "${task.agent}")`);
104
- }
105
- }
106
- // Type-check e enum apenas: quem cobra o veredito contra o código é o
107
- // reuse-validator, no `morph-spec verify`.
108
- if (task.reuse !== undefined) {
109
- if (!Array.isArray(task.reuse)) {
110
- errors.push(`${ctx}.reuse must be an array of { concept, verdict, target?, why? }`);
111
- } else {
112
- task.reuse.forEach((entry, j) => {
113
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
114
- errors.push(`${ctx}.reuse[${j}] must be an object { concept, verdict, target?, why? }`);
115
- return;
116
- }
117
- if (typeof entry.concept !== 'string' || !entry.concept.trim()) {
118
- errors.push(`${ctx}.reuse[${j}].concept must be a non-empty string`);
119
- }
120
- if (!VALID_REUSE_VERDICT.includes(entry.verdict)) {
121
- errors.push(`${ctx}.reuse[${j}].verdict must be one of ${VALID_REUSE_VERDICT.join(', ')}`);
122
- }
123
- if (entry.target !== undefined && typeof entry.target !== 'string') {
124
- errors.push(`${ctx}.reuse[${j}].target must be a string path`);
125
- }
126
- if (entry.why !== undefined && typeof entry.why !== 'string') {
127
- errors.push(`${ctx}.reuse[${j}].why must be a string`);
128
- }
129
- });
130
- }
131
- }
132
- if (task.standards !== undefined) {
133
- if (!Array.isArray(task.standards)) {
134
- errors.push(`${ctx}.standards must be an array of paths`);
135
- } else {
136
- task.standards.forEach((s, i) => {
137
- if (typeof s !== 'string' || !s.trim()) {
138
- errors.push(`${ctx}.standards[${i}] must be a non-empty string path`);
139
- }
140
- });
141
- }
142
- }
143
- }
144
-
145
- return { valid: errors.length === 0, errors };
146
- }
147
-
148
- /**
149
- * Read and parse tasks.json from disk. Throws on validation errors.
150
- * @param {string} filePath - Absolute path to tasks.json
151
- * @returns {Promise<object>}
152
- */
153
- export async function parseTasksJson(filePath) {
154
- const data = await fs.readJson(filePath);
155
- const { valid, errors } = validateTasksJson(data);
156
- if (!valid) {
157
- throw new Error(`Invalid tasks.json:\n ${errors.join('\n ')}`);
158
- }
159
- return data;
160
- }
161
-
162
- /**
163
- * Lenient variant for read-only analysis commands (e.g. `morph-spec dag`):
164
- * a missing `version` field is defaulted to "1.0" instead of failing
165
- * validation — legacy tasks.json predating the field is still analyzable.
166
- * All other schema errors still throw exactly like `parseTasksJson`.
167
- *
168
- * @param {string} filePath - Absolute path to tasks.json
169
- * @returns {Promise<{ data: object, versionDefaulted: boolean }>}
170
- */
171
- export async function parseTasksJsonLenient(filePath) {
172
- const data = await fs.readJson(filePath);
173
- const versionDefaulted = data.version === undefined;
174
- if (versionDefaulted) data.version = '1.0';
175
-
176
- const { valid, errors } = validateTasksJson(data);
177
- if (!valid) {
178
- throw new Error(`Invalid tasks.json:\n ${errors.join('\n ')}`);
179
- }
180
- return { data, versionDefaulted };
181
- }
182
-
183
- /**
184
- * Synchronous variant — used by hooks.
185
- */
186
- export function parseTasksJsonSync(filePath) {
187
- if (!existsSync(filePath)) {
188
- throw new Error(`tasks.json not found: ${filePath}`);
189
- }
190
- const data = JSON.parse(readFileSync(filePath, 'utf8'));
191
- const { valid, errors } = validateTasksJson(data);
192
- if (!valid) {
193
- throw new Error(`Invalid tasks.json:\n ${errors.join('\n ')}`);
194
- }
195
- return data;
196
- }
197
-
198
- /**
199
- * Teto de `outputs` por task: a task inteira tem de caber num dispatch. Medido
200
- * em campo (chat-demo-simulacao): tasks de 12 a 30 outputs levaram 6 de 10
201
- * sub-agents ao teto de turnos, e cada retomada custou um contexto frio.
202
- */
203
- export const MAX_TASK_OUTPUTS = 8;
204
-
205
- /**
206
- * A régua única de "task grande demais", lida pelo `dag` e pelo `gate-check`.
207
- * Fora de `validateTasksJson` de propósito: `outputs` segue mutável depois do
208
- * Gate 2, e o teto num write de status travaria feature em voo.
209
- *
210
- * @returns {{ id: string, outputs: number }[]}
211
- */
212
- export function oversizedTasks(data) {
213
- const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
214
- return tasks
215
- .filter(t => Array.isArray(t?.outputs) && t.outputs.length > MAX_TASK_OUTPUTS)
216
- .map(t => ({ id: t.id, outputs: t.outputs.length }));
217
- }
218
-
219
- /**
220
- * @returns {object[]} tasks with status === 'pending'
221
- */
222
- export function getPendingTasks(data) {
223
- return data.tasks.filter(t => t.status === 'pending');
224
- }
225
-
226
- /**
227
- * Tasks ready to run: pending AND all dependencies are 'done'.
228
- */
229
- export function getReadyTasks(data) {
230
- const doneIds = new Set(data.tasks.filter(t => t.status === 'done').map(t => t.id));
231
- return data.tasks.filter(t =>
232
- t.status === 'pending' && t.dependencies.every(dep => doneIds.has(dep))
233
- );
234
- }
235
-
236
- /**
237
- * Mutate `data` to set task status. Returns `data` for chaining.
238
- */
239
- export function updateTaskStatus(data, taskId, newStatus) {
240
- if (!VALID_STATUS.includes(newStatus)) {
241
- throw new Error(`Invalid status: ${newStatus}`);
242
- }
243
- const task = data.tasks.find(t => t.id === taskId);
244
- if (!task) throw new Error(`Task not found: ${taskId}`);
245
- task.status = newStatus;
246
- return data;
247
- }
248
-
249
- /**
250
- * Write tasks.json to disk. Validates before writing.
251
- */
252
- export async function writeTasksJson(filePath, data) {
253
- const { valid, errors } = validateTasksJson(data);
254
- if (!valid) {
255
- throw new Error(`Cannot write invalid tasks.json:\n ${errors.join('\n ')}`);
256
- }
257
- await fs.writeJson(filePath, data, { spaces: 2 });
258
- }
259
-
260
- /**
261
- * Group tasks by `group` field, falling back to 'backend' when absent.
262
- * Returns `{ [group]: taskId[] }` shape matching the legacy parseAndGroupTasks output.
263
- */
264
- export function groupTasks(data) {
265
- const groups = {};
266
- for (const task of data.tasks) {
267
- const group = task.group || 'backend';
268
- if (!groups[group]) groups[group] = [];
269
- groups[group].push(task.id);
270
- }
271
- return groups;
272
- }
273
-
274
- /**
275
- * Convert tasks.json to the legacy taskList shape used in state.json.
276
- * Lets the rest of the codebase keep using feature.taskList while migration
277
- * is in progress.
278
- *
279
- * BOUNDARY — this is the ONE place `done` becomes `completed`, and it is a
280
- * one-way translation OUT of the tasks.json vocabulary. In tasks.json the
281
- * terminal status is `done` (tasks.schema.json's enum has no `completed`);
282
- * `completed` belongs to the legacy taskList and to Claude Code's native
283
- * session tasks. Leaking `completed` back into a tasks.json write is rejected
284
- * by `protect-spec-files` — see framework/rules/morph-task-tracking.md, which
285
- * spells the split out for the author.
286
- */
287
- export function toTaskList(data) {
288
- return data.tasks.map(t => ({
289
- id: t.id,
290
- title: t.title,
291
- status: t.status === 'done' ? 'completed' : t.status,
292
- dependencies: [...t.dependencies],
293
- files: t.outputs || [],
294
- checkpoint: null,
295
- }));
296
- }