@polymorphism-tech/morph-spec 8.46.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 (304) hide show
  1. package/CLAUDE.md +53 -44
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +38 -55
  4. package/bin/validate.js +5 -134
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  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/create-umbraco-lp.md +2 -0
  12. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  13. package/framework/commands/morph-apply.md +5 -28
  14. package/framework/commands/morph-archive.md +2 -2
  15. package/framework/commands/morph-hotfix.md +8 -8
  16. package/framework/commands/morph-infra.md +17 -203
  17. package/framework/commands/morph-media.md +3 -0
  18. package/framework/commands/morph-preflight.md +7 -31
  19. package/framework/commands/morph-proposal.md +2 -50
  20. package/framework/commands/morph-status.md +3 -3
  21. package/framework/commands/morph-troubleshoot.md +2 -14
  22. package/framework/docs/CLI.md +585 -286
  23. package/framework/evals/review-checklist.md +82 -41
  24. package/framework/hooks/README.md +10 -11
  25. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  26. package/framework/hooks/claude-code/core/denylist.json +33 -3
  27. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  28. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  30. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  31. package/framework/hooks/claude-code/core/state-sync.js +17 -19
  32. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  33. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  34. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  35. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  36. package/framework/hooks/claude-code/statusline.py +55 -41
  37. package/framework/hooks/package.json +3 -0
  38. package/framework/hooks/shared/activity-logger.js +1 -1
  39. package/framework/hooks/shared/build-check.js +10 -6
  40. package/framework/hooks/shared/command-tokens.js +169 -0
  41. package/framework/hooks/shared/comment-rules.js +1 -1
  42. package/framework/hooks/shared/comment-scanner.js +10 -23
  43. package/framework/hooks/shared/comment-stripper.js +1 -1
  44. package/framework/hooks/shared/e2e-check.js +67 -31
  45. package/framework/hooks/shared/errata.js +72 -0
  46. package/framework/hooks/shared/feature-locator.js +1 -1
  47. package/framework/hooks/shared/frontend-locations.js +6 -0
  48. package/framework/hooks/shared/gate-question.js +18 -0
  49. package/framework/hooks/shared/phase-utils.js +15 -32
  50. package/framework/hooks/shared/slice-progress.js +118 -0
  51. package/framework/hooks/shared/spawn-command.js +23 -0
  52. package/framework/hooks/shared/state-reader.js +61 -87
  53. package/framework/hooks/shared/state-writer.js +3 -3
  54. package/framework/mcp/registry.json +3 -4
  55. package/framework/phases.json +23 -39
  56. package/framework/rules/ai-agents-standards.md +18 -16
  57. package/framework/rules/asaas-standards.md +3 -1
  58. package/framework/rules/csharp-standards.md +10 -8
  59. package/framework/rules/data-standards.md +5 -5
  60. package/framework/rules/dotnet-templates.md +1 -1
  61. package/framework/rules/frontend-standards.md +4 -2
  62. package/framework/rules/gsap-standards.md +10 -5
  63. package/framework/rules/hangfire-standards.md +3 -1
  64. package/framework/rules/infrastructure-standards.md +4 -2
  65. package/framework/rules/neon-auth-standards.md +3 -1
  66. package/framework/rules/neon-standards.md +6 -4
  67. package/framework/rules/nextjs-standards.md +13 -11
  68. package/framework/rules/nextjs-templates.md +1 -1
  69. package/framework/rules/resend-standards.md +3 -1
  70. package/framework/schemas/config.schema.json +14 -9
  71. package/framework/schemas/feature.schema.json +15 -52
  72. package/framework/skills/README.md +4 -5
  73. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  74. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  75. package/framework/skills/core/morph-apply/SKILL.md +150 -223
  76. package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
  77. package/framework/skills/core/morph-machine/SKILL.md +4 -4
  78. package/framework/skills/core/morph-media/SKILL.md +3 -3
  79. package/framework/skills/core/morph-plan/SKILL.md +138 -300
  80. package/framework/skills/core/morph-review/SKILL.md +112 -137
  81. package/framework/skills/core/morph-standards/SKILL.md +29 -41
  82. package/framework/skills/core/morph-uiux/SKILL.md +78 -153
  83. package/framework/standards/STANDARDS.json +90 -1219
  84. package/framework/standards/STANDARDS.meta.json +1305 -0
  85. package/framework/standards/ai-agents/evals-with-cache.md +2 -3
  86. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  87. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  88. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  89. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  90. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  91. package/framework/standards/ai-agents/setup.md +2 -2
  92. package/framework/standards/ai-agents/structured-output.md +1 -1
  93. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  94. package/framework/standards/architecture/code-comments.md +8 -8
  95. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  96. package/framework/standards/backend/api/validation.md +135 -47
  97. package/framework/standards/backend/database/postgresql/database.md +11 -7
  98. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  99. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  100. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  101. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  102. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  103. package/framework/standards/backend/dotnet/testing.md +1 -1
  104. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  105. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  106. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  107. package/framework/standards/data/nosql/cache/redis.md +120 -44
  108. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  109. package/framework/standards/frontend/design-system/animations.md +1 -1
  110. package/framework/standards/frontend/design-system/naming.md +1 -1
  111. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  112. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  113. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  114. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  115. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  116. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  117. package/framework/templates/README.md +2 -3
  118. package/framework/templates/REGISTRY.json +6 -170
  119. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  120. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  121. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  122. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  123. package/framework/templates/docs/plan.md +72 -0
  124. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  127. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  128. package/framework/templates/infrastructure/github/README.md +2 -1
  129. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  130. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  131. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  132. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  133. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  134. package/package.json +1 -1
  135. package/src/commands/e2e/e2e.js +2 -2
  136. package/src/commands/observability/cost.js +18 -60
  137. package/src/commands/observability/telemetry.js +3 -5
  138. package/src/commands/project/doctor-procs.js +1 -1
  139. package/src/commands/project/doctor.js +9 -9
  140. package/src/commands/project/fleet.js +4 -4
  141. package/src/commands/project/heavy.js +3 -3
  142. package/src/commands/project/init.js +6 -1
  143. package/src/commands/project/setup-infra-cmd.js +2 -1
  144. package/src/commands/project/stack-scan.js +58 -0
  145. package/src/commands/project/standards.js +79 -25
  146. package/src/commands/project/status.js +29 -69
  147. package/src/commands/project/update.js +33 -19
  148. package/src/commands/project/worktree.js +1 -1
  149. package/src/commands/state/advance.js +62 -46
  150. package/src/commands/state/approve.js +95 -138
  151. package/src/commands/state/archive.js +20 -84
  152. package/src/commands/state/auto-approval.js +68 -0
  153. package/src/commands/state/create.js +28 -1
  154. package/src/commands/state/delete.js +1 -1
  155. package/src/commands/state/finish.js +11 -23
  156. package/src/commands/state/gate-check.js +86 -95
  157. package/src/commands/state/gate-decision.js +6 -4
  158. package/src/commands/state/handoff.js +1 -1
  159. package/src/commands/state/list.js +9 -9
  160. package/src/commands/state/report-check.js +157 -311
  161. package/src/commands/state/retype.js +9 -3
  162. package/src/commands/validation/mutate.js +77 -14
  163. package/src/commands/validation/plan-check.js +81 -0
  164. package/src/commands/validation/validate-feature.js +60 -135
  165. package/src/commands/validation/verify.js +41 -82
  166. package/src/core/paths/output-schema.js +20 -67
  167. package/src/core/state/gate-order.js +5 -6
  168. package/src/core/state/lifecycle-audit.js +2 -2
  169. package/src/core/state/state-manager.js +17 -98
  170. package/src/core/templates/template-registry.js +2 -2
  171. package/src/core/templates/template-seed.js +22 -0
  172. package/src/core/work-types.js +10 -10
  173. package/src/dashboard/app/App.jsx +0 -2
  174. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  175. package/src/dashboard/app/components/CommandPalette.jsx +7 -8
  176. package/src/dashboard/app/components/EntityLink.jsx +1 -2
  177. package/src/dashboard/app/components/Markdown.jsx +1 -1
  178. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  179. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  180. package/src/dashboard/app/lib/phases.js +2 -4
  181. package/src/dashboard/app/pages/Activity.jsx +1 -1
  182. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  183. package/src/dashboard/app/pages/Features.jsx +9 -9
  184. package/src/dashboard/app/pages/Guide.jsx +2 -2
  185. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  186. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  187. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  188. package/src/dashboard/app/pages/Standards.jsx +1 -1
  189. package/src/dashboard/scanner/activity-scanner.js +6 -16
  190. package/src/dashboard/scanner/agents-scanner.js +1 -1
  191. package/src/dashboard/scanner/docs-scanner.js +5 -23
  192. package/src/dashboard/scanner/features-scanner.js +95 -60
  193. package/src/dashboard/scanner/index.js +0 -2
  194. package/src/dashboard/scanner/project-api.js +1 -1
  195. package/src/dashboard/scanner/rules-scanner.js +15 -10
  196. package/src/dashboard/scanner/skills-scanner.js +2 -3
  197. package/src/dashboard/scanner/standards-scanner.js +2 -1
  198. package/src/lib/ci/ci-verify.js +17 -1
  199. package/src/lib/ci/github-ci.js +1 -1
  200. package/src/lib/detectors/index.js +3 -8
  201. package/src/lib/generators/handoff-generator.js +34 -65
  202. package/src/lib/generators/settings-generator.js +11 -89
  203. package/src/lib/git/evidence-staleness.js +1 -1
  204. package/src/lib/git/tree-fingerprint.js +19 -7
  205. package/src/lib/git/tree-state.js +2 -2
  206. package/src/lib/heavy/cleanup.js +1 -5
  207. package/src/lib/installers/memory-installer.js +6 -8
  208. package/src/lib/mutation/commands.js +3 -2
  209. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  210. package/src/lib/mutation/plan.js +14 -6
  211. package/src/lib/mutation/runner.js +1 -1
  212. package/src/lib/mutation/subset.js +46 -9
  213. package/src/lib/plan/artifact-vocabulary.js +60 -0
  214. package/src/lib/plan/checks.js +234 -0
  215. package/src/lib/plan/plan-parser.js +209 -0
  216. package/src/lib/plan/plan-template.js +18 -0
  217. package/src/lib/review-reports.js +222 -0
  218. package/src/lib/risk-detector.js +134 -135
  219. package/src/lib/specs/adr-index.js +50 -69
  220. package/src/lib/specs/design-seed.js +18 -0
  221. package/src/lib/specs/frontmatter.js +44 -0
  222. package/src/lib/specs/gfm-table.js +17 -0
  223. package/src/lib/specs/old-format.js +68 -0
  224. package/src/lib/specs/revocations.js +37 -95
  225. package/src/lib/specs/screen-checklist.js +18 -17
  226. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  227. package/src/lib/stack/stack-profile.js +1 -1
  228. package/src/lib/stack/stack-scan.js +267 -0
  229. package/src/lib/standards/registry.js +43 -0
  230. package/src/lib/standards/sections.js +58 -0
  231. package/src/lib/tasks/eval-node.js +4 -12
  232. package/src/lib/tasks/test-nodes.js +1 -74
  233. package/src/lib/tasks/test-runner.js +75 -117
  234. package/src/lib/telemetry/metrics.js +8 -69
  235. package/src/lib/telemetry/telemetry-reader.js +5 -118
  236. package/src/lib/telemetry/verify-event.js +5 -7
  237. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  238. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  239. package/src/lib/validators/reuse/reuse-validator.js +24 -48
  240. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  241. package/src/lib/validators/traceability-check.js +85 -129
  242. package/src/lib/validators/validation-runner.js +8 -88
  243. package/src/lib/validators/verify-runner.js +134 -301
  244. package/src/lib/worktree/docker-audit.js +1 -2
  245. package/src/lib/worktree/fleet-scanner.js +25 -38
  246. package/src/scripts/setup-infra.js +42 -22
  247. package/src/utils/agents-installer.js +92 -13
  248. package/src/utils/claude-settings-manager.js +0 -35
  249. package/src/utils/file-copier.js +34 -6
  250. package/src/utils/hooks-installer.js +16 -9
  251. package/src/utils/skills-installer.js +1 -2
  252. package/framework/commands/commit.md +0 -166
  253. package/framework/evals/trace-schema.md +0 -67
  254. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  255. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  256. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  257. package/framework/hooks/shared/tasks-validator.js +0 -111
  258. package/framework/rubrics/backend.json +0 -41
  259. package/framework/rubrics/default.json +0 -36
  260. package/framework/rubrics/frontend.json +0 -36
  261. package/framework/rules/morph-task-tracking.md +0 -54
  262. package/framework/rules/morph-workflow.md +0 -244
  263. package/framework/schemas/tasks.schema.json +0 -79
  264. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  265. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  266. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  267. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  268. package/framework/templates/docs/proposal.md +0 -192
  269. package/framework/templates/docs/spec.md +0 -57
  270. package/framework/templates/docs/ui-components.md +0 -52
  271. package/framework/templates/docs/ui-design-system.md +0 -76
  272. package/framework/templates/docs/ui-flows.md +0 -167
  273. package/framework/templates/examples/spec-examples.md +0 -91
  274. package/framework/templates/feature/decisions.md +0 -124
  275. package/framework/templates/feature/recap.md +0 -145
  276. package/framework/templates/project/validate.js +0 -97
  277. package/framework/templates/tasks.template.json +0 -17
  278. package/src/commands/state/usecase.js +0 -143
  279. package/src/commands/tasks/dag.js +0 -181
  280. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  281. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  282. package/src/lib/agents/output-collector.js +0 -76
  283. package/src/lib/evaluator-report.js +0 -91
  284. package/src/lib/specs/acervo-format.js +0 -70
  285. package/src/lib/specs/acervo-lifecycle.js +0 -264
  286. package/src/lib/specs/done-criteria-check.js +0 -200
  287. package/src/lib/specs/usecase-check.js +0 -301
  288. package/src/lib/specs/usecase-promoter.js +0 -301
  289. package/src/lib/specs/usecase-registry.js +0 -237
  290. package/src/lib/tasks/dispatch-planner.js +0 -319
  291. package/src/lib/tasks/mermaid-renderer.js +0 -132
  292. package/src/lib/tasks/task-classifier.js +0 -30
  293. package/src/lib/tasks/task-parser.js +0 -209
  294. package/src/lib/tasks/tasks-json-parser.js +0 -296
  295. package/src/lib/tasks/test-scope.js +0 -597
  296. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  297. package/src/lib/validators/architecture/index.js +0 -7
  298. package/src/lib/validators/css/css-validator.js +0 -340
  299. package/src/lib/validators/css/index.js +0 -7
  300. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  301. package/src/lib/validators/design-system/index.js +0 -7
  302. package/src/lib/validators/score-aggregator.js +0 -244
  303. package/src/lib/validators/ui/index.js +0 -7
  304. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -1,264 +0,0 @@
1
- /**
2
- * Lifecycle of the project's acervo: a use case stops being the current
3
- * behaviour when a later feature supersedes it.
4
- *
5
- * status: superseded
6
- * superseded_by: UC-589
7
- * scope: "vitrine pública, fora de `.morph-admin-shell`" ← only when partial
8
- *
9
- * Three roles live here:
10
- * - the ONE rule for that frontmatter (`supersessionProblems`) — the check
11
- * reads it, and the writer below only ever produces what it accepts;
12
- * - the close-time writer (`applySpecRevocations`): what the feature's
13
- * `spec.md` declared it revokes lands on `.morph/specs/usecases/` and on
14
- * the ADR index, fail-open like the promotion it follows;
15
- * - the plan-time check (`checkLifecycle`), shared by `dag` and `usecase
16
- * check`, run while `spec.md` can still be fixed.
17
- */
18
-
19
- import { readFileSync, writeFileSync } from 'fs';
20
- import { join } from 'path';
21
- import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
22
- import {
23
- SPECS_USECASES_PATHSPEC,
24
- hasFrontmatter,
25
- readFrontmatterField,
26
- removeFrontmatterField,
27
- setFrontmatterField,
28
- } from './usecase-promoter.js';
29
- import { SUPERSEDED, UC_ID_RE, useCaseFilesById } from './acervo-format.js';
30
- import { parseRevocations } from './revocations.js';
31
- import { ADR_INDEX_PATHSPEC, readAdrIndex, updateAdrIndex } from './adr-index.js';
32
-
33
- export { SUPERSEDED };
34
-
35
- const UC = OUTPUT_SCHEMA.usecases;
36
- const ACERVO_LABEL = SPECS_USECASES_PATHSPEC.replace(/\\/g, '/');
37
- const ADR_INDEX_LABEL = `${ADR_INDEX_PATHSPEC.replace(/\\/g, '/')}/`;
38
-
39
- /** @returns {{ status: string|null, supersededBy: string|null, scope: string|null }} */
40
- export function lifecycleOf(content) {
41
- return {
42
- status: readFrontmatterField(content, 'status'),
43
- supersededBy: readFrontmatterField(content, 'superseded_by'),
44
- scope: readFrontmatterField(content, 'scope'),
45
- };
46
- }
47
-
48
- /**
49
- * The rule for a use case's supersession frontmatter.
50
- *
51
- * @param {string} content
52
- * @param {string} id - the use case's own id
53
- * @returns {string[]} one line per problem; `[]` = well formed
54
- */
55
- export function supersessionProblems(content, id) {
56
- const { status, supersededBy, scope } = lifecycleOf(content);
57
- const problems = [];
58
- if (status === SUPERSEDED) {
59
- if (!supersededBy) problems.push('status: superseded sem superseded_by');
60
- else if (!UC_ID_RE.test(supersededBy)) problems.push(`superseded_by "${supersededBy}" não é um UC-{nn}`);
61
- else if (supersededBy === id) problems.push('superseded_by aponta para o próprio use case');
62
- } else if (supersededBy !== null || scope !== null) {
63
- problems.push(`${supersededBy !== null ? 'superseded_by' : 'scope'} sem status: superseded`);
64
- }
65
- if (scope !== null && !String(scope).trim()) problems.push('scope vazio');
66
- return problems;
67
- }
68
-
69
- function readOrNull(path) {
70
- try { return readFileSync(path, 'utf8'); } catch { return null; }
71
- }
72
-
73
- const specPathOf = (featureDir) => join(featureDir, OUTPUT_SCHEMA.spec.phaseDir, OUTPUT_SCHEMA.spec.filename);
74
-
75
- /** A finished feature cannot be re-closed: the fix is the next feature's revocation row. */
76
- const FIX_NEXT_TIME = 'corrija a linha na seção de revogações do spec.md — a feature já arquivada não é reprocessada, ' +
77
- 'então a revogação corrigida entra no spec.md da próxima feature';
78
-
79
- /**
80
- * Mark the acervo use cases the entries revoke. Runs after the promotion, so
81
- * the acervo already holds every substitute this feature brought — except the
82
- * ones the promotion refused, which only LOOK present (the file under that id
83
- * belongs to another feature).
84
- *
85
- * @returns {{ superseded: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
86
- * problems: Array<{id: string, reason: string, message: string}> }}
87
- */
88
- function supersedeUseCases(cwd, entries, refusedIds) {
89
- const superseded = [];
90
- const problems = [];
91
- if (!entries.length) return { superseded, problems };
92
-
93
- const dir = join(cwd, SPECS_USECASES_PATHSPEC);
94
- const files = useCaseFilesById(dir);
95
- const problem = (id, reason, message) => problems.push({ id, reason, message });
96
-
97
- for (const e of entries) {
98
- const targets = files.get(e.id) ?? [];
99
- if (targets.length === 0) {
100
- problem(e.id, 'not-in-acervo', `${e.id} não está no acervo (${ACERVO_LABEL}/) — nada foi marcado; ${FIX_NEXT_TIME}`);
101
- continue;
102
- }
103
- if (targets.length > 1) {
104
- problem(e.id, 'duplicate-in-acervo',
105
- `${e.id} tem ${targets.length} arquivos no acervo (${targets.join(', ')}) — nenhum foi marcado; o acervo precisa de um arquivo por UC`);
106
- continue;
107
- }
108
- if (refusedIds.has(e.substitute)) {
109
- problem(e.id, 'substitute-refused',
110
- `o substituto ${e.substitute} de ${e.id} não foi promovido (colisão de numeração) — ${e.id} não foi marcado`);
111
- continue;
112
- }
113
- if (!files.has(e.substitute)) {
114
- problem(e.id, 'substitute-not-found',
115
- `o substituto ${e.substitute} de ${e.id} não está no acervo — ${e.id} não foi marcado; ${FIX_NEXT_TIME}`);
116
- continue;
117
- }
118
-
119
- const file = targets[0];
120
- try {
121
- const path = join(dir, file);
122
- const before = readFileSync(path, 'utf8');
123
- if (!hasFrontmatter(before)) {
124
- problem(e.id, 'no-frontmatter', `${ACERVO_LABEL}/${file} não tem frontmatter — o status não foi aplicado`);
125
- continue;
126
- }
127
- let next = setFrontmatterField(before, 'status', SUPERSEDED);
128
- next = setFrontmatterField(next, 'superseded_by', e.substitute);
129
- // JSON-quoted: free text with `: ` or a leading backtick breaks a bare YAML scalar.
130
- next = e.scope
131
- ? setFrontmatterField(next, 'scope', JSON.stringify(e.scope))
132
- : removeFrontmatterField(next, 'scope');
133
- if (next !== before) writeFileSync(path, next, 'utf8');
134
- superseded.push({ id: e.id, file, supersededBy: e.substitute, ...(e.scope ? { scope: e.scope } : {}) });
135
- } catch {
136
- problem(e.id, 'unwritable', `${ACERVO_LABEL}/${file} não pôde ser lido/gravado — o status não foi aplicado`);
137
- }
138
- }
139
- return { superseded, problems };
140
- }
141
-
142
- /**
143
- * Apply, at close time, what the feature's `spec.md` declared it revokes.
144
- * Fail-open: nothing here may refuse the close; what did not apply comes back
145
- * in `problems`, next to the row's own parse problems.
146
- *
147
- * @param {string} cwd - project root that owns `.morph/specs/`
148
- * @param {string} featureDir - the feature folder (already moved to the archive)
149
- * @param {{ refused?: Array<{ id: string }> }} [promotion] - the promotion's result, run just before
150
- * @returns {{ superseded: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
151
- * adrIndex: ReturnType<typeof updateAdrIndex>|null,
152
- * problems: Array<{id: string|null, reason: string, message: string}> }}
153
- */
154
- export function applySpecRevocations(cwd, featureDir, promotion = {}) {
155
- const out = { superseded: [], adrIndex: null, problems: [] };
156
-
157
- const spec = readOrNull(specPathOf(featureDir));
158
- const parsed = spec === null ? { entries: [], problems: [] } : parseRevocations(spec);
159
- out.problems.push(...parsed.problems);
160
-
161
- try {
162
- const refusedIds = new Set((promotion.refused ?? []).map((r) => r.id));
163
- const uc = supersedeUseCases(cwd, parsed.entries.filter((e) => e.kind === 'uc'), refusedIds);
164
- out.superseded = uc.superseded;
165
- out.problems.push(...uc.problems);
166
- } catch { /* fail-open */ }
167
-
168
- try {
169
- out.adrIndex = updateAdrIndex(cwd, featureDir, parsed.entries.filter((e) => e.kind === 'adr'));
170
- out.problems.push(...out.adrIndex.problems);
171
- } catch { /* fail-open */ }
172
-
173
- return out;
174
- }
175
-
176
- /**
177
- * Plan-time check of a feature against the acervo's lifecycle.
178
- *
179
- * Warnings only, never errors: a superseded use case is a question for the
180
- * plan, and the revocation rows are re-reported at close time anyway.
181
- *
182
- * @param {{ cwd: string, featureDir: string, tasksData?: object|null }} input
183
- * @returns {{ warnings: string[] }}
184
- */
185
- export function checkLifecycle({ cwd, featureDir, tasksData = null }) {
186
- const warnings = [];
187
- const acervoDir = join(cwd, SPECS_USECASES_PATHSPEC);
188
- const ownDir = join(featureDir, UC.phaseDir, UC.dirname);
189
- const acervo = useCaseFilesById(acervoDir);
190
- const own = useCaseFilesById(ownDir);
191
-
192
- const ownLife = new Map();
193
- for (const [id, files] of own) {
194
- const file = files[files.length - 1];
195
- const content = readOrNull(join(ownDir, file));
196
- if (content === null) continue;
197
- ownLife.set(id, lifecycleOf(content));
198
- for (const p of supersessionProblems(content, id)) warnings.push(`2-plan/usecases/${file}: ${p}`);
199
- }
200
-
201
- // The acervo can hold hundreds of use cases: read only the ones this plan touches.
202
- const acervoLife = new Map();
203
- const acervoOf = (id) => {
204
- if (acervoLife.has(id)) return acervoLife.get(id);
205
- const files = acervo.get(id) ?? [];
206
- if (files.length > 1) warnings.push(`${ACERVO_LABEL}/: ${id} tem ${files.length} arquivos (${files.join(', ')}) — o acervo precisa de um arquivo por UC`);
207
- const file = files[files.length - 1];
208
- const content = file ? readOrNull(join(acervoDir, file)) : null;
209
- const life = content === null ? null : lifecycleOf(content);
210
- if (content !== null) {
211
- for (const p of supersessionProblems(content, id)) warnings.push(`${ACERVO_LABEL}/${file}: ${p}`);
212
- }
213
- acervoLife.set(id, life);
214
- return life;
215
- };
216
-
217
- const spec = readOrNull(specPathOf(featureDir));
218
- const rev = spec === null ? { entries: [], problems: [] } : parseRevocations(spec);
219
- for (const p of rev.problems) warnings.push(`spec.md, revogações: ${p.message}`);
220
-
221
- const revokedHere = new Map();
222
- let indexedAdrs = null;
223
- for (const e of rev.entries) {
224
- if (e.kind === 'adr') {
225
- indexedAdrs ??= new Set(readAdrIndex(cwd).map((r) => r.ref));
226
- if (!indexedAdrs.has(e.id)) {
227
- warnings.push(`spec.md, revogações: ${e.id} não está no índice de ADRs ativos (${ADR_INDEX_LABEL}) — o finish não terá o que retirar`);
228
- }
229
- continue;
230
- }
231
- revokedHere.set(e.id, e);
232
- if (!acervo.has(e.id)) {
233
- warnings.push(`spec.md, revogações: ${e.id} não está no acervo (${ACERVO_LABEL}/) — o finish não terá o que marcar`);
234
- } else {
235
- acervoOf(e.id);
236
- }
237
- if (!acervo.has(e.substitute) && !own.has(e.substitute)) {
238
- warnings.push(`spec.md, revogações: o substituto ${e.substitute} de ${e.id} não existe no acervo nem em 2-plan/usecases/ — o finish não aplicará a linha`);
239
- }
240
- }
241
-
242
- const tasks = Array.isArray(tasksData?.tasks) ? tasksData.tasks : [];
243
- for (const task of tasks) {
244
- const id = task?.usecase;
245
- if (!id) continue;
246
- const inAcervo = acervoOf(id);
247
- const inOwn = ownLife.get(id);
248
- const here = revokedHere.get(id);
249
-
250
- let where = null;
251
- if (inAcervo?.status === SUPERSEDED) where = { src: 'está superseded no acervo', ...inAcervo };
252
- else if (inOwn?.status === SUPERSEDED) where = { src: 'está superseded na cópia da feature', ...inOwn };
253
- else if (here) where = { src: 'é revogado por esta feature (spec.md)', supersededBy: here.substitute, scope: here.scope };
254
- if (!where) continue;
255
-
256
- const by = where.supersededBy;
257
- const detail = [by ? `substituto: ${by}` : 'sem substituto declarado', where.scope ? `ainda vale em: ${where.scope}` : null]
258
- .filter(Boolean).join('; ');
259
- const advice = where.scope ? 'confira que a task realiza a parte que ainda vale' : by ? `cite ${by}` : 'confira o acervo';
260
- warnings.push(`${task.id}.usecase → ${id} ${where.src} (${detail}) — ${advice}`);
261
- }
262
-
263
- return { warnings };
264
- }
@@ -1,200 +0,0 @@
1
- /**
2
- * Lexical heuristic: does a task's `doneCriteria` describe an ARTIFACT (a
3
- * file, a grep, a binary) instead of behaviour observable at runtime?
4
- *
5
- * A literal `doneCriteria` travels verbatim into the test that verifies it
6
- * (`morph-implement` copies it, never paraphrases). A use-case post-condition
7
- * shaped like "nenhum arquivo em X contém Y" — even a literal `grep` command —
8
- * becomes a test that reads file text instead of exercising behaviour. This
9
- * is a WARNING, never an error: `dag`'s exit code never changes because of
10
- * it, the same way `checkUseCases`'s coverage/literalness findings do not.
11
- *
12
- * Runs over EVERY task, unlike `usecase-check.js#checkUseCases` (which
13
- * short-circuits when the feature has no use cases): a task's own
14
- * `doneCriteria` can describe an artifact with no use case in sight, and
15
- * that must still be caught. This is why it lives in its own file — it is a
16
- * standalone lexical pass, not wiring between use cases and tasks.
17
- *
18
- * Two ways a match is silenced:
19
- * - a task WITHOUT a use case can justify itself inline, with
20
- * `Por que artefato: …` anywhere in its own `doneCriteria`. A task WITH a
21
- * use case never can — it justifies in the use case, otherwise the marker
22
- * would silence every post-condition it carries at once;
23
- * - a task carrying a use-case post-condition literally is silenced only
24
- * if THAT post-condition was justified in the use case document (the
25
- * `> [!note] Por que artefato:` callout `usecase-check.js` parses).
26
- * Justified text is removed from the task's `doneCriteria` before scanning —
27
- * not skipped as a whole task — so a justified P1 never silences an
28
- * unjustified P2 carried in the same `doneCriteria`. The scan runs on the
29
- * whitespace-collapsed text with its CASE PRESERVED: an uppercase format
30
- * acronym (`arquivo em CSV`) is the only thing that tells an export from a
31
- * directory (`arquivo em src`), and lowercasing first would erase it.
32
- */
33
-
34
- import { ARTIFACT_MARKER_RE, loadUseCases, normalize } from './usecase-check.js';
35
-
36
- /**
37
- * Case-insensitive spelling of `word` inside a regex that must stay
38
- * case-SENSITIVE elsewhere — Node 18 has no inline `(?i:…)` modifier.
39
- */
40
- const anyCase = (word) => [...word].map((c) => {
41
- const lo = c.toLowerCase();
42
- const up = c.toUpperCase();
43
- return lo === up ? c : `[${lo}${up}]`;
44
- }).join('');
45
-
46
- /**
47
- * What follows "arquivo em" in a behavioural export criterion ("exporta o
48
- * arquivo em CSV", "salva o arquivo em formato aberto"): an uppercase file
49
- * format acronym or the word `formato`. Case-sensitive on purpose — `src`,
50
- * `docs` or `lib` after "arquivo em" are places, and places are artifacts.
51
- */
52
- const FORMAT_TOKEN = String.raw`\s+(?:[A-Z]{2,5}(?![\p{L}\d_])|${anyCase('formato')}(?![\p{L}]))`;
53
-
54
- /**
55
- * Vocabulary of artifact-shaped phrasing. `term` is the label surfaced in
56
- * warnings and cited by the `morph-plan` skill prose — keep it stable, a
57
- * later parity test imports it verbatim.
58
- *
59
- * Unicode lookarounds, never `\b`: in JS, `\b` does not treat accented
60
- * letters (é, á, ó…) as word characters, so it would false-boundary inside
61
- * "não" or "binário".
62
- */
63
- export const ARTIFACT_VOCABULARY = [
64
- { term: 'grep', re: /(?<![\p{L}\d_])grep(?![\p{L}\d_])/iu },
65
- { term: 'contém a string', re: /cont[ée]m\s+as?\s+strings?/iu },
66
- { term: 'o binário', re: /(?<![\p{L}])o\s+bin[áa]rio(?![\p{L}])/iu },
67
- { term: 'nenhum arquivo', re: /(?<![\p{L}])nenhum\s+arquivo(?![\p{L}])/iu },
68
- {
69
- term: 'arquivo/pasta/diretório em',
70
- re: new RegExp(
71
- String.raw`(?<![\p{L}])(?:${anyCase('arquivo')}|${anyCase('pasta')}|${anyCase('diret')}[óoÓO]${anyCase('rio')})` +
72
- String.raw`[sS]?\s+${anyCase('em')}(?![\p{L}])(?!${FORMAT_TOKEN})`,
73
- 'u',
74
- ),
75
- },
76
- { term: 'glob **/', re: /\*\*\/|\/\*\*/ },
77
- ];
78
-
79
- /**
80
- * Vocabulary terms matched in `text`, in vocabulary order, each at most once.
81
- *
82
- * @param {string} text
83
- * @returns {string[]}
84
- */
85
- export function findArtifactTerms(text) {
86
- const s = String(text ?? '');
87
- const found = [];
88
- for (const { term, re } of ARTIFACT_VOCABULARY) {
89
- if (re.test(s)) found.push(term);
90
- }
91
- return found;
92
- }
93
-
94
- /** Whitespace-collapsed, case PRESERVED — `normalize` without the lowercasing. */
95
- function collapse(text) {
96
- return String(text).replace(/\s+/g, ' ').trim();
97
- }
98
-
99
- /**
100
- * Remove every case-insensitive occurrence of `needle` (already normalized)
101
- * from `text`, keeping the case of what remains. Relies on `text` and its
102
- * lowercase form being index-aligned — the caller guarantees it.
103
- */
104
- function cut(text, needle) {
105
- if (!needle) return text;
106
- const lower = text.toLowerCase();
107
- let out = '';
108
- let from = 0;
109
- let at;
110
- while ((at = lower.indexOf(needle, from)) !== -1) {
111
- out += `${text.slice(from, at)} `;
112
- from = at + needle.length;
113
- }
114
- return out + text.slice(from);
115
- }
116
-
117
- /**
118
- * Flag every task whose `doneCriteria` describes an artifact rather than
119
- * behaviour, after removing use-case text the author already justified.
120
- *
121
- * Fail-open: a missing `usecasesDir` still checks every task (it just has no
122
- * justifications or post-conditions to consult); a malformed `tasksData`
123
- * (no `tasks` array) or a non-string `doneCriteria` never throws.
124
- *
125
- * @param {Object} tasksData - parsed tasks.json
126
- * @param {string} usecasesDir - absolute path to `2-plan/usecases`
127
- * @returns {{ warnings: string[], flagged: Array<{ task: string, usecase: string|null, labels: string[], terms: string[] }>, checked: true }}
128
- */
129
- export function checkArtifactCriteria(tasksData, usecasesDir) {
130
- const warnings = [];
131
- const flagged = [];
132
- const tasks = Array.isArray(tasksData?.tasks) ? tasksData.tasks : [];
133
-
134
- let byId = new Map();
135
- if (typeof usecasesDir === 'string') {
136
- try { byId = loadUseCases(usecasesDir); } catch { byId = new Map(); }
137
- }
138
-
139
- for (const task of tasks) {
140
- if (typeof task?.doneCriteria !== 'string') continue;
141
-
142
- let D = collapse(task.doneCriteria);
143
- if (!D) continue;
144
- // `cut` needs `D` index-aligned with its lowercase form. The rare letter
145
- // whose lowercase is longer (`İ`) breaks that; fall back to the lowercase
146
- // text, which only costs the format-acronym exception for that task.
147
- if (D.toLowerCase().length !== D.length) D = D.toLowerCase();
148
-
149
- // Inline justification is for tasks WITHOUT a use case (morph-plan: "Task
150
- // sem `usecase` justifica no próprio doneCriteria"). On a use-case task it
151
- // would silence every post-condition carried, justified or not.
152
- if (!task.usecase && ARTIFACT_MARKER_RE.test(D)) continue;
153
-
154
- const uc = task.usecase ? byId.get(task.usecase) : null;
155
- const unjustifiedLabels = [];
156
-
157
- if (uc) {
158
- // Remove justified post-condition text first, so its own vocabulary
159
- // hits (if any) never surface — and so it cannot mask what remains.
160
- // A wrapped post-condition is removed in both forms a plan legitimately
161
- // carries: the whole block, or only its first line (what `checkUseCases`
162
- // treats as THE post-condition). Longest first.
163
- for (const pc of uc.postConditions) {
164
- if (!uc.artifactJustified.has(pc.label)) continue;
165
- for (const text of [uc.artifactJustified.get(pc.label), pc.text]) {
166
- D = cut(D, normalize(text));
167
- }
168
- }
169
- // Among what is left, name the unjustified post-conditions actually
170
- // carried into this doneCriteria AND that themselves read as artifact —
171
- // judged on the whole block, since a wrapped post-condition can carry
172
- // its vocabulary on the continuation line.
173
- const carried = D.toLowerCase();
174
- for (const pc of uc.postConditions) {
175
- if (uc.artifactJustified.has(pc.label)) continue;
176
- const needle = normalize(pc.text);
177
- const block = uc.fullText.get(pc.label) ?? pc.text;
178
- if (needle && carried.includes(needle) && findArtifactTerms(block).length > 0) {
179
- unjustifiedLabels.push(pc.label);
180
- }
181
- }
182
- }
183
-
184
- const terms = findArtifactTerms(D);
185
- if (terms.length === 0) continue;
186
-
187
- const quoted = terms.join(', ');
188
- const message = unjustifiedLabels.length > 0
189
- ? `${task.id}.doneCriteria descreve artefato, não comportamento ("${quoted}") — ` +
190
- `reescreva ${task.usecase} ${unjustifiedLabels.join('/')} como efeito observável em runtime, ` +
191
- `ou justifique com "> [!note] Por que artefato:" na linha abaixo dela`
192
- : `${task.id}.doneCriteria descreve artefato, não comportamento ("${quoted}") — ` +
193
- `reescreva como efeito observável em runtime, ou acrescente "Por que artefato: …" ao próprio critério`;
194
-
195
- warnings.push(message);
196
- flagged.push({ task: task.id, usecase: task.usecase || null, labels: unjustifiedLabels, terms });
197
- }
198
-
199
- return { warnings, flagged, checked: true };
200
- }