@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
@@ -44,8 +44,8 @@ import {
44
44
  getActiveFeature,
45
45
  getFeaturePhase,
46
46
  getPendingGates,
47
- getTaskStats,
48
47
  } from '../../shared/state-reader.js';
48
+ import { sliceProgress } from '../../shared/slice-progress.js';
49
49
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
50
50
  import { acquireClaim, foreignClaim, claimAge } from '../../shared/claim-registry.js';
51
51
  import { rotateActivityLogIfLarge } from '../../shared/activity-logger.js';
@@ -232,10 +232,11 @@ function buildActiveFeatureSummary(dir) {
232
232
  if (!active) return null;
233
233
 
234
234
  const phase = getFeaturePhase(active.name, dir);
235
- const stats = getTaskStats(active.feature);
235
+ const slices = sliceProgress(join(dir, '.morph', 'features', active.name));
236
236
  const pending = getPendingGates(active.name, dir);
237
237
 
238
- let text = `MORPH-SPEC: feature ativa ${active.name} (fase ${phase}) · tasks ${stats.completed}/${stats.total}`;
238
+ let text = `MORPH-SPEC: feature ativa ${active.name} (fase ${phase})`;
239
+ if (slices) text += ` · fatias ${slices.done}/${slices.total}`;
239
240
  if (pending.length) {
240
241
  text += ` · gates pendentes: ${pending.join(', ')}`;
241
242
  }
@@ -264,7 +265,7 @@ function claimBlock(dir, feature, sessionId) {
264
265
  if (!other) return '';
265
266
  return `\n⚠️ ATENÇÃO — a feature ${feature} tem claim VIVO de OUTRA sessão`
266
267
  + `\n pid ${other.pid}${other.path ? ` · ${other.path}` : ''} · visto há ${claimAge(other)}`
267
- + '\n Duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent'
268
+ + '\n Duas sessões na mesma feature já custaram um relatório de fatia inteiro e um sub-agent'
268
269
  + '\n (o worktree sumiu debaixo dele). Confirme com a outra sessão ANTES de agir —'
269
270
  + '\n principalmente antes de `finish`, `archive` ou `delete`.'
270
271
  + `\n Se aquela sessão morreu: morph-spec claim ${feature} --release`;
@@ -56,28 +56,26 @@ SEP = f" {GRAY}│{R} "
56
56
 
57
57
  # ── MORPH framework constants ────────────────────────────────────────────────
58
58
 
59
- # Mirrors derivePhase() in src/core/state/state-manager.js — the first phase
60
- # (scanning backwards) whose CANONICAL artifact exists wins. An empty folder
61
- # is not enough.
59
+ # Mirrors derivePhaseFrom() in framework/hooks/shared/state-reader.js — the
60
+ # first phase (scanning backwards) whose CANONICAL artifact exists wins. An
61
+ # empty folder is not enough; a canonical ending in '/' is any .md under it.
62
62
  PHASE_MAP = [
63
- ('4-review', 'review', 'review-report.md'),
64
- ('3-implement', 'implement', 'recap.md'),
65
- ('2-plan', 'plan', 'spec.md'),
63
+ ('4-review', 'review', 'verifier.md'),
64
+ ('3-implement', 'implement', 'reports/'),
66
65
  ('1-design', 'uiux', 'design-system.md'),
67
- ('0-proposal', 'proposal', 'proposal.md'),
66
+ ('2-plan', 'plan', 'plan.md'),
68
67
  ]
69
- PHASE_INDEX = {'proposal': 0, 'uiux': 1, 'plan': 2, 'implement': 3, 'review': 4}
68
+ PHASE_INDEX = {'plan': 0, 'uiux': 1, 'implement': 2, 'review': 3}
70
69
  PHASE_LABELS = {
71
- 'proposal': 'proposal',
72
- 'uiux': 'UI/UX',
73
70
  'plan': 'planning',
71
+ 'uiux': 'UI/UX',
74
72
  'implement': 'implement',
75
73
  'review': 'review',
76
74
  }
77
- PIPELINE_TOTAL = 5
75
+ PIPELINE_TOTAL = 4
78
76
 
79
77
  # Gates that can block; `implement` has none. Mirrors feature.schema.json.
80
- GATE_PHASES = ('proposal', 'uiux', 'plan', 'review')
78
+ GATE_PHASES = ('plan', 'uiux', 'review')
81
79
 
82
80
 
83
81
  # ── Auto-compact threshold ───────────────────────────────────────────────────
@@ -358,49 +356,65 @@ def get_worktree_label(payload, cwd):
358
356
  return None
359
357
 
360
358
 
361
- def _gate_implied_phase(gates):
362
- """Phase implied purely by approved gates (mirrors state-manager.js)."""
359
+ def _gate_implied_phase(gates, design_started):
360
+ """Phase implied purely by approved gates (mirrors state-reader.js)."""
363
361
  def approved(gate):
364
362
  return (gates.get(gate) or {}).get('approved') is True
365
363
  if approved('review'):
366
364
  return 'review'
367
365
  if approved('plan'):
368
- return 'implement'
369
- if approved('uiux'):
370
- return 'plan'
371
- return 'proposal'
366
+ return 'uiux' if design_started and not approved('uiux') else 'implement'
367
+ return 'plan'
368
+
369
+
370
+ def _has_artifact(folder, canonical):
371
+ if not canonical.endswith('/'):
372
+ return (folder / canonical).exists()
373
+ try:
374
+ return any(p.is_file() for p in (folder / canonical).glob('*.md'))
375
+ except OSError:
376
+ return False
372
377
 
373
378
 
374
379
  def derive_phase(feature_dir, gates):
375
380
  """Phase from the filesystem, bumped by approved gates."""
376
- artifact_phase = 'proposal'
381
+ artifact_phase = 'plan'
377
382
  for folder, phase, canonical in PHASE_MAP:
378
- if (feature_dir / folder / canonical).exists():
383
+ if _has_artifact(feature_dir / folder, canonical):
379
384
  artifact_phase = phase
380
385
  break
381
- implied = _gate_implied_phase(gates)
386
+ implied = _gate_implied_phase(gates, (feature_dir / '1-design').exists())
382
387
  if PHASE_INDEX[implied] > PHASE_INDEX[artifact_phase]:
383
388
  return implied
384
389
  return artifact_phase
385
390
 
386
391
 
387
- def count_tasks(feature_dir, feature_json):
388
- """(done, total) from 2-plan/tasks.json — the declared source of truth.
392
+ PARTIAL_MARKER = re.compile(r'^[\s#>*_]*PARCIAL\b')
389
393
 
390
- `skipped` counts as resolved: it will never be worked on, and leaving it
391
- out would pin the bar below 100%. Falls back to feature.json's aggregate
392
- counters, which the schema marks as informational.
394
+
395
+ def count_slices(feature_dir):
396
+ """(done, total) of the slices in 2-plan/checks.json.
397
+
398
+ Same rule as framework/hooks/shared/slice-progress.js: a slice is done when
399
+ 3-implement/reports/{id}.md exists and its first line is not PARCIAL.
393
400
  """
394
401
  try:
395
- raw = (feature_dir / '2-plan' / 'tasks.json').read_text(encoding='utf-8')
396
- tasks = json.loads(raw).get('tasks')
397
- if isinstance(tasks, list) and tasks:
398
- done = sum(1 for task in tasks if task.get('status') in ('done', 'skipped'))
399
- return done, len(tasks)
402
+ slices = json.loads((feature_dir / '2-plan' / 'checks.json').read_text(encoding='utf-8')).get('slices')
400
403
  except Exception:
401
- pass
402
- aggregate = feature_json.get('tasks') or {}
403
- return safe_int(aggregate.get('completed')), safe_int(aggregate.get('total'))
404
+ return 0, 0
405
+ if not isinstance(slices, list):
406
+ return 0, 0
407
+ ids = [s.get('id') for s in slices if isinstance(s, dict) and isinstance(s.get('id'), str)]
408
+ done = 0
409
+ for slice_id in ids:
410
+ try:
411
+ text = (feature_dir / '3-implement' / 'reports' / f'{slice_id}.md').read_text(encoding='utf-8')
412
+ except Exception:
413
+ continue
414
+ first = next((line for line in text.splitlines() if line.strip()), '')
415
+ if not PARTIAL_MARKER.match(first):
416
+ done += 1
417
+ return done, len(ids)
404
418
 
405
419
 
406
420
  def get_active_features(cwd):
@@ -428,15 +442,15 @@ def get_active_features(cwd):
428
442
  feature_dir = path.parent
429
443
  gates = data.get('approvalGates') or {}
430
444
  phase = derive_phase(feature_dir, gates)
431
- done, total = count_tasks(feature_dir, data)
445
+ done, total = count_slices(feature_dir)
432
446
  pending = None
433
447
  if phase in GATE_PHASES and (gates.get(phase) or {}).get('approved') is not True:
434
448
  pending = phase
435
449
  result.append({
436
450
  'name': feature_dir.name,
437
451
  'phase': phase,
438
- 'tasks_done': done,
439
- 'tasks_total': total,
452
+ 'slices_done': done,
453
+ 'slices_total': total,
440
454
  'pending': pending,
441
455
  })
442
456
  return result
@@ -484,7 +498,7 @@ def render_morph_line(feature, hook_name, columns):
484
498
  """Line 0 — the active MORPH feature.
485
499
 
486
500
  Goes through join_to_width like the status line: a feature name plus phase,
487
- task bar, gate and hook is the longest thing this script prints, and a
501
+ slice bar, gate and hook is the longest thing this script prints, and a
488
502
  wrapping line 0 would defeat the whole point of the single-row layout.
489
503
  """
490
504
  parts = [(0, f"{CYAN}{BOLD}► {feature['name']}{R}")]
@@ -497,12 +511,12 @@ def render_morph_line(feature, hook_name, columns):
497
511
  else:
498
512
  parts.append((1, f"{CYAN}{label} {position + 1}/{PIPELINE_TOTAL}{R}"))
499
513
 
500
- if feature['tasks_total'] > 0:
501
- pct = feature['tasks_done'] / feature['tasks_total'] * 100
514
+ if feature['slices_total'] > 0:
515
+ pct = feature['slices_done'] / feature['slices_total'] * 100
502
516
  parts.append((
503
517
  3,
504
518
  f"{GREEN}{progress_bar(pct, 6)} "
505
- f"{feature['tasks_done']}/{feature['tasks_total']} tasks{R}"
519
+ f"{feature['slices_done']}/{feature['slices_total']} fatias{R}"
506
520
  ))
507
521
 
508
522
  if feature['pending']:
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * Exported names/signatures are unchanged from the previous doc-based
20
20
  * implementation on purpose — hooks outside this file's ownership
21
- * (gate-guard.js, tasks-json-guard.js, enforce-phase-writes.js,
21
+ * (gate-guard.js, enforce-phase-writes.js,
22
22
  * loop-detect.js, protect-spec-files.js, validate-completion.js) call
23
23
  * logHookActivity/logEvent and must keep working untouched. Only the
24
24
  * on-disk format changed.
@@ -14,7 +14,7 @@ import { join } from 'path';
14
14
  const DOTNET_BUILD_CMD = 'dotnet build --no-restore --nologo -v q';
15
15
  const TSC_BUILD_CMD = 'npx tsc --noEmit';
16
16
 
17
- function isDotnetStack(s) {
17
+ export function isDotnetStack(s) {
18
18
  return s.includes('dotnet') || s.includes('aspnet');
19
19
  }
20
20
 
@@ -70,16 +70,20 @@ export function getBuildCommand(stack, hasTsConfig = false) {
70
70
  * `getBuildCommand()`'s existing single-stack behavior.
71
71
  *
72
72
  * @param {string} stack
73
- * @param {{ cwd: string, frontendPath?: string, backendPath?: string }} opts
73
+ * `backendTarget` names the solution/project to build (relative to the backend
74
+ * directory) when that directory holds none itself.
75
+ *
76
+ * @param {{ cwd: string, frontendPath?: string, backendPath?: string, backendTarget?: string }} opts
74
77
  * @returns {Array<{ command: string, cwd: string }>}
75
78
  */
76
- export function getBuildSteps(stack, { cwd, frontendPath, backendPath } = {}) {
79
+ export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTarget } = {}) {
77
80
  if (!stack || !cwd) return [];
78
81
  const s = stack.toLowerCase();
79
82
  const steps = [];
80
83
 
81
84
  if (isDotnetStack(s)) {
82
- steps.push({ command: DOTNET_BUILD_CMD, cwd: backendPath ? join(cwd, backendPath) : cwd });
85
+ const command = backendTarget ? DOTNET_BUILD_CMD.replace(/^dotnet build/, `dotnet build "${backendTarget}"`) : DOTNET_BUILD_CMD;
86
+ steps.push({ command, cwd: backendPath ? join(cwd, backendPath) : cwd });
83
87
  }
84
88
 
85
89
  if (isFrontendStack(s)) {
@@ -129,11 +133,11 @@ export function parseBuildResult(exitCode, output) {
129
133
  * @param {string} cwd - Project root directory
130
134
  * @param {string} stack - Project stack string
131
135
  * @param {number} [timeout=30000] - Timeout in ms
132
- * @param {{ frontendPath?: string, backendPath?: string }} [paths] - Monorepo sub-paths, from config.json → project
136
+ * @param {{ frontendPath?: string, backendPath?: string, backendTarget?: string }} [paths] - Monorepo sub-paths, from config.json → project
133
137
  * @returns {{ status: 'pass'|'fail'|'skip', output: string }}
134
138
  */
135
139
  export function runBuildCheck(cwd, stack, timeout = 30000, paths = {}) {
136
- const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath });
140
+ const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget });
137
141
  if (steps.length === 0) return { status: 'skip', output: '' };
138
142
 
139
143
  const failures = [];
@@ -117,3 +117,172 @@ export function exitCodeMasked(command) {
117
117
  const last = segments[segments.length - 1] || '';
118
118
  return (segments.length > 1 && REPORTER.test(last)) || /\|\|/.test(last.replace(/"[^"]*"|'[^']*'/g, ''));
119
119
  }
120
+
121
+ const WRITE_REDIRECT = /^(?:[1&])?>>?\|?/;
122
+
123
+ function quotedTokens(text) {
124
+ const tokens = [];
125
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
126
+ let m;
127
+ while ((m = re.exec(text)) !== null) {
128
+ tokens.push({ text: m[1] ?? m[2] ?? m[3], quoted: m[3] === undefined, single: m[2] !== undefined });
129
+ }
130
+ return tokens;
131
+ }
132
+
133
+ const CHAIN = /^(?:&&|\|\||\||\|&)$/;
134
+ const ASSIGNMENT = /^([A-Za-z_]\w*)=(.*)$/;
135
+ const REDIRECT = /^(?:\d|&)?[<>]/;
136
+ const SCRIPT_RUNNER = /^(?:python[\d.]*|py|node|bun|deno)$/;
137
+
138
+ function simpleCommands(head) {
139
+ const commands = [];
140
+ for (const segment of topLevelSegments(head).segments) {
141
+ let current = [];
142
+ for (const token of quotedTokens(segment)) {
143
+ if (!token.quoted && CHAIN.test(token.text)) { commands.push(current); current = []; } else current.push(token);
144
+ }
145
+ commands.push(current);
146
+ }
147
+ return commands.filter((c) => c.length > 0);
148
+ }
149
+
150
+ function positionals(args, takesValue) {
151
+ const out = [];
152
+ const values = [];
153
+ for (let i = 0; i < args.length; i++) {
154
+ const { text, quoted } = args[i];
155
+ if (!quoted && REDIRECT.test(text)) { if (/[<>]$/.test(text)) i++; continue; }
156
+ if (!quoted && text.startsWith('-') && text !== '-') {
157
+ if (takesValue(text) && i + 1 < args.length) values.push(args[++i].text);
158
+ continue;
159
+ }
160
+ out.push(text);
161
+ }
162
+ return { files: out, values };
163
+ }
164
+
165
+ function sedTargets(args) {
166
+ const inPlace = args.some(({ text, quoted }) => !quoted && (/^-[A-Za-z]*i/.test(text) || text.startsWith('--in-place')));
167
+ if (!inPlace) return [];
168
+ const { files, values } = positionals(args, (o) => /^-[A-Za-z]*[ef]$/.test(o) || o === '--expression' || o === '--file');
169
+ return values.length > 0 ? files : files.slice(1);
170
+ }
171
+
172
+ function perlTargets(args) {
173
+ if (!args.some(({ text, quoted }) => !quoted && /^-[A-Za-z]*i/.test(text))) return [];
174
+ const { files, values } = positionals(args, (o) => /^-[A-Za-z.]*[eE]$/.test(o));
175
+ return values.length > 0 ? files : files.slice(1);
176
+ }
177
+
178
+ function copyTargets(args) {
179
+ if (args.some(({ text }) => text === '-t' || text.startsWith('--target-directory'))) return [];
180
+ const { files } = positionals(args, () => false);
181
+ if (files.length < 2) return [];
182
+ const dest = files[files.length - 1];
183
+ if (!dest.endsWith('/')) return [dest];
184
+ return files.slice(0, -1).map((src) => dest + src.replace(/^.*\//, ''));
185
+ }
186
+
187
+ const PY_OPEN_WRITE = /\bopen\(\s*([^,()]+?)\s*,\s*(?:mode\s*=\s*)?(['"])([^'"]*)\2/g;
188
+ const JS_WRITE = /\b(?:writeFileSync|writeFile|appendFileSync|appendFile)\(\s*([^,()]+?)\s*,/g;
189
+ const PY_PATH_WRITE = /\.write_(?:text|bytes)\(/;
190
+ const STRING_LITERAL = /(['"`])((?:(?!\1)[^\\\n]|\\.)*)\1/g;
191
+ const PATH_LITERAL = /^[\w@./-]*[\w-]\.[A-Za-z]{1,5}$/;
192
+
193
+ function literalOf(expr) {
194
+ const m = /^(['"`])([^'"`]*)\1$/.exec(expr.trim());
195
+ return m ? m[2] : null;
196
+ }
197
+
198
+ // A write call with a computed path (`open(p, "w")`) counts every path-shaped literal in the script.
199
+ function scriptWrites(script) {
200
+ const literals = [];
201
+ let computed = PY_PATH_WRITE.test(script);
202
+ for (const m of script.matchAll(PY_OPEN_WRITE)) {
203
+ if (!/[wax+]/.test(m[3])) continue;
204
+ const lit = literalOf(m[1]);
205
+ if (lit === null) computed = true; else literals.push(lit);
206
+ }
207
+ for (const m of script.matchAll(JS_WRITE)) {
208
+ const lit = literalOf(m[1]);
209
+ if (lit === null) computed = true; else literals.push(lit);
210
+ }
211
+ if (!computed) return literals;
212
+ const mentioned = [...script.matchAll(STRING_LITERAL)].map((m) => m[2]).filter((s) => PATH_LITERAL.test(s));
213
+ return [...new Set([...literals, ...mentioned])];
214
+ }
215
+
216
+ function runnerScript(args, body) {
217
+ for (let i = 0; i < args.length; i++) {
218
+ if (['-c', '-e', '-p', '--eval', '--print'].includes(args[i].text) && !args[i].quoted) return args[i + 1]?.text ?? null;
219
+ }
220
+ const { files } = positionals(args, () => false);
221
+ return (files.length === 0 || files[0] === '-') ? body : null;
222
+ }
223
+
224
+ function heredocBody(lines, at) {
225
+ if (at < 0) return null;
226
+ const delim = /<<-?\s*(['"]?)([\w.-]+)\1/.exec(lines[at])?.[2];
227
+ if (!delim) return null;
228
+ const end = lines.findIndex((line, i) => i > at && line.trim() === delim);
229
+ return lines.slice(at + 1, end < 0 ? undefined : end).join('\n');
230
+ }
231
+
232
+ function commandWriteTargets(head, body) {
233
+ const vars = new Map();
234
+ const expand = ({ text, single }) => (single ? text : text.replace(/\$\{?([A-Za-z_]\w*)\}?/g, (all, name) => vars.get(name) ?? all));
235
+ const targets = [];
236
+ for (const tokens of simpleCommands(head)) {
237
+ let i = 0;
238
+ for (; i < tokens.length && !tokens[i].quoted && ASSIGNMENT.test(tokens[i].text); i++) {
239
+ const [, name, value] = ASSIGNMENT.exec(tokens[i].text);
240
+ vars.set(name, value);
241
+ }
242
+ if (i >= tokens.length) continue;
243
+ const word = tokens[i].text.replace(/^.*[\\/]/, '').replace(/\.exe$/i, '');
244
+ const args = tokens.slice(i + 1).map((t) => ({ ...t, text: expand(t) }));
245
+ if (word === 'sed') targets.push(...sedTargets(args));
246
+ else if (word === 'perl') targets.push(...perlTargets(args));
247
+ else if (word === 'cp' || word === 'mv') targets.push(...copyTargets(args));
248
+ else if (SCRIPT_RUNNER.test(word)) {
249
+ const script = runnerScript(args, body);
250
+ if (script) targets.push(...scriptWrites(script));
251
+ }
252
+ }
253
+ return targets;
254
+ }
255
+
256
+ /**
257
+ * Files a command writes: stdout redirection (`>`, `>>`, `1>`, `&>`), `tee`, `sed -i`,
258
+ * `perl -i`, the `cp`/`mv` destination, and a python/node `-c`/`-e`/heredoc script's writes.
259
+ * The redirect scan stops at the line that opens a heredoc: the body is data, and an
260
+ * apostrophe in it would derail the quote-aware split. Quoted operators are text.
261
+ * @param {string} command
262
+ * @returns {string[]}
263
+ */
264
+ export function writeTargets(command) {
265
+ const lines = String(command || '').split('\n');
266
+ const heredocAt = lines.findIndex((line) => line.includes('<<'));
267
+ const head = (heredocAt >= 0 ? lines.slice(0, heredocAt + 1) : lines).join('\n');
268
+ const tokens = quotedTokens(head);
269
+
270
+ const targets = [];
271
+ for (let i = 0; i < tokens.length; i++) {
272
+ const { text, quoted } = tokens[i];
273
+ if (quoted) continue;
274
+ const op = text.match(WRITE_REDIRECT);
275
+ if (op) {
276
+ const rest = text.slice(op[0].length);
277
+ const target = rest || tokens[i + 1]?.text;
278
+ if (target && !target.startsWith('&')) targets.push(target);
279
+ continue;
280
+ }
281
+ if (text.replace(/^.*[\/]/, '') === 'tee') {
282
+ for (let j = i + 1; j < tokens.length && !/^[;&|]/.test(tokens[j].text); j++) {
283
+ if (!tokens[j].text.startsWith('-')) targets.push(tokens[j].text);
284
+ }
285
+ }
286
+ }
287
+ return [...new Set([...targets, ...commandWriteTargets(head, heredocBody(lines, heredocAt))])];
288
+ }
@@ -2,7 +2,7 @@
2
2
  * The comment shapes the `architecture-code-comments` standard forbids in
3
3
  * every case, judged only on the comments an edit ADDED. Whether a short
4
4
  * comment restates the code is not decidable here; that stays with the
5
- * Gate 3 evaluator and the `code-budget` density.
5
+ * Gate 3 reviewer and the `code-budget` density.
6
6
  *
7
7
  * @module comment-rules
8
8
  */
@@ -10,42 +10,29 @@
10
10
 
11
11
  import { stripComments } from './comment-stripper.js';
12
12
 
13
- /** Vocabulário de processo do morph-spec, fora o id de task (abaixo). */
13
+ /**
14
+ * Id de cenário (`C{n}`) e de fatia (`F{n}`) do plano. A mesma grafia também é
15
+ * tecla de função, métrica, célula de planilha ou o modelo C-quatro: esses
16
+ * vizinhos não contam (casos em comment-scanner.test.js).
17
+ */
18
+ const PLAN_ID = new RegExp(String.raw`(?<!\b(?:key|tecla|press|aperte|cell|c[eé]lula|column|coluna)\s+)(?<![\w.#$/-])[CF]\d{1,2}(?![\w.(-]|\s+(?:key|tecla|score|model|modelo|diagram|diagrama)\b)`, 'i');
19
+
14
20
  const PROCESS_CITATION = [
15
21
  /\bUC[-\s]?\d+/,
16
22
  /\bADR[-\s]?\d+/,
17
23
  /\bFR[-\s]?\d+/,
18
24
  /\bgate\s?[1-3]\b/i,
19
- /\bspec\.md\b/i,
25
+ /\bplan\.md\b/i,
26
+ /\bchecks\.json\b/i,
20
27
  /\bmockups?\b/i,
21
28
  ];
22
29
 
23
- /**
24
- * `T{n}` também é nome de parâmetro genérico. Não casa depois de `template`/
25
- * `typename`/`generic`, nem colado a letra ou dígito (a hora ISO `24T10`).
26
- */
27
- const TASK_ID = /(?<!\b(?:[Tt]emplate|[Tt]ypename|[Gg]eneric)\s+)(?<![\w"])T\d{1,3}\b(?!")/;
28
-
29
- /** Lista de tipos `<…>` e `{…}` (`Func<T1, T2>`, `cref="Func{T1,T2}"`, XML doc), de dentro para fora. */
30
- const TYPE_LIST = /<[^<>]*>|\{[^{}]*\}/g;
31
-
32
- function withoutTypeLists(text) {
33
- let previous;
34
- let current = text;
35
- do {
36
- previous = current;
37
- current = current.replace(TYPE_LIST, ' ');
38
- } while (current !== previous);
39
- return current;
40
- }
41
-
42
30
  /**
43
31
  * @param {string} commentText
44
32
  * @returns {boolean} o comentário usa o vocabulário de processo acima
45
33
  */
46
34
  export function citesProcess(commentText) {
47
- return PROCESS_CITATION.some((re) => re.test(commentText))
48
- || TASK_ID.test(withoutTypeLists(commentText));
35
+ return PROCESS_CITATION.some((re) => re.test(commentText)) || PLAN_ID.test(commentText);
49
36
  }
50
37
 
51
38
  /**
@@ -10,7 +10,7 @@
10
10
  * - `registry-audit.js` — precisa ler **JSONC**, a forma que o exemplar
11
11
  * canônico `model-registry.v2.json` usa e que o leitor C# do kit aceita de
12
12
  * propósito (`JsonCommentHandling.Skip`, `AllowTrailingCommas`);
13
- * - `dotnet-test-introspection.js` — lê o `global.json` por `jsoncToJson`;
13
+ * - `mutation/dotnet-introspection.js` — lê o `global.json` por `jsoncToJson`;
14
14
  * - `comment-scanner.js` (ao lado; lido pelo `code-budget` e pelo hook
15
15
  * `comment-guard`) — acha as linhas de
16
16
  * comentário de `.cs`/`.ts`/`.js`, e é o único que liga a opção `js`.