@polymorphism-tech/morph-spec 9.0.0 → 9.2.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 (345) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +11 -63
  4. package/bin/validate.js +1 -39
  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 +2 -1
  9. package/framework/MORPH.md +3 -3
  10. package/framework/agents.json +19 -11
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +22 -31
  14. package/framework/evals/review-checklist.md +12 -2
  15. package/framework/hooks/README.md +8 -44
  16. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  17. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  18. package/framework/hooks/claude-code/core/chain.js +4 -65
  19. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  20. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  21. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  22. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  23. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  24. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  25. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  26. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  27. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  28. package/framework/hooks/claude-code/core/state-sync.js +12 -68
  29. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  30. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  31. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  32. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  33. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  34. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  35. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  36. package/framework/hooks/claude-code/statusline.sh +11 -11
  37. package/framework/hooks/shared/activity-logger.js +9 -89
  38. package/framework/hooks/shared/agent-reap.js +2 -17
  39. package/framework/hooks/shared/build-check.js +6 -75
  40. package/framework/hooks/shared/claim-registry.js +9 -119
  41. package/framework/hooks/shared/command-tokens.js +6 -51
  42. package/framework/hooks/shared/comment-rules.js +5 -20
  43. package/framework/hooks/shared/comment-scanner.js +6 -25
  44. package/framework/hooks/shared/comment-stripper.js +11 -85
  45. package/framework/hooks/shared/e2e-check.js +0 -0
  46. package/framework/hooks/shared/errata.js +2 -15
  47. package/framework/hooks/shared/feature-locator.js +11 -95
  48. package/framework/hooks/shared/frontend-locations.js +1 -3
  49. package/framework/hooks/shared/gate-question.js +1 -7
  50. package/framework/hooks/shared/git-exclude.js +3 -20
  51. package/framework/hooks/shared/git-paths.js +8 -104
  52. package/framework/hooks/shared/graph-freshness.js +22 -166
  53. package/framework/hooks/shared/graph-refresh.js +21 -149
  54. package/framework/hooks/shared/heavy-command.js +4 -12
  55. package/framework/hooks/shared/hook-response.js +8 -83
  56. package/framework/hooks/shared/morph-cli.js +8 -52
  57. package/framework/hooks/shared/payload-utils.js +10 -132
  58. package/framework/hooks/shared/phase-utils.js +1 -42
  59. package/framework/hooks/shared/redact-secrets.js +4 -15
  60. package/framework/hooks/shared/reparse-scan.js +6 -66
  61. package/framework/hooks/shared/slice-progress.js +4 -23
  62. package/framework/hooks/shared/source-files.js +4 -17
  63. package/framework/hooks/shared/state-reader.js +13 -183
  64. package/framework/hooks/shared/state-writer.js +8 -52
  65. package/framework/hooks/shared/stdin-reader.js +1 -11
  66. package/framework/hooks/shared/telemetry-logger.js +7 -67
  67. package/framework/hooks/shared/transcript-usage.js +7 -29
  68. package/framework/hooks/shared/worktree-root.js +7 -29
  69. package/framework/rules/csharp-standards.md +1 -1
  70. package/framework/rules/frontend-standards.md +1 -0
  71. package/framework/rules/hangfire-standards.md +1 -1
  72. package/framework/rules/neon-standards.md +1 -1
  73. package/framework/rules/nextjs-standards.md +1 -1
  74. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  75. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  76. package/framework/skills/core/morph-apply/SKILL.md +4 -4
  77. package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
  78. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  79. package/framework/skills/core/morph-media/SKILL.md +1 -1
  80. package/framework/skills/core/morph-plan/SKILL.md +9 -7
  81. package/framework/skills/core/morph-review/SKILL.md +9 -6
  82. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  83. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  84. package/framework/standards/STANDARDS.json +36 -14
  85. package/framework/standards/STANDARDS.meta.json +16 -2
  86. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  87. package/framework/standards/ai-agents/agent-session.md +1 -1
  88. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  89. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  90. package/framework/standards/ai-agents/guardrails.md +1 -1
  91. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  92. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  93. package/framework/standards/ai-agents/production.md +1 -1
  94. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  95. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  96. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  97. package/framework/standards/ai-agents/setup.md +2 -2
  98. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  99. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  100. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  101. package/framework/standards/ai-agents/workflows.md +1 -1
  102. package/framework/standards/architecture/code-comments.md +1 -1
  103. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  104. package/framework/standards/backend/api/validation.md +1 -1
  105. package/framework/standards/backend/dotnet/testing.md +10 -4
  106. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  107. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  108. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  109. package/framework/standards/data/nosql/cache/redis.md +1 -1
  110. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  111. package/framework/standards/frontend/admin-app/forms.md +1 -1
  112. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  113. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  114. package/framework/standards/frontend/nextjs/testing.md +10 -6
  115. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  116. package/framework/templates/.idea/morph-templates.xml +92 -0
  117. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  118. package/framework/templates/CLAUDE.md.core.template +3 -3
  119. package/framework/templates/IDE-SNIPPETS.md +262 -262
  120. package/framework/templates/REGISTRY.json +9 -9
  121. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  122. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  123. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  124. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  125. package/framework/templates/docs/plan.md +3 -1
  126. package/framework/templates/docs/ui-mockups.md +4 -0
  127. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  128. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  129. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  130. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  131. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  132. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  133. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  134. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  135. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  136. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  137. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  138. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  139. package/framework/templates/state.template.json +17 -17
  140. package/framework/templates/ui/design-system.css +226 -226
  141. package/package.json +1 -1
  142. package/src/commands/context/context-refresh.js +0 -16
  143. package/src/commands/dashboard/dashboard.js +2 -8
  144. package/src/commands/e2e/e2e.js +17 -104
  145. package/src/commands/graph/affected.js +0 -14
  146. package/src/commands/graph/build.js +11 -50
  147. package/src/commands/graph/explain.js +0 -16
  148. package/src/commands/graph/status.js +1 -12
  149. package/src/commands/mcp/mcp-setup.js +4 -62
  150. package/src/commands/observability/cost.js +5 -57
  151. package/src/commands/observability/telemetry.js +1 -14
  152. package/src/commands/project/branch.js +4 -45
  153. package/src/commands/project/claim.js +4 -48
  154. package/src/commands/project/doctor.js +48 -302
  155. package/src/commands/project/env.js +1 -27
  156. package/src/commands/project/fleet.js +3 -32
  157. package/src/commands/project/init.js +4 -68
  158. package/src/commands/project/install-plugin-cmd.js +0 -1
  159. package/src/commands/project/standards.js +1 -62
  160. package/src/commands/project/status.js +0 -53
  161. package/src/commands/project/update.js +31 -239
  162. package/src/commands/project/worktree.js +108 -882
  163. package/src/commands/scaffold/scaffold.js +0 -25
  164. package/src/commands/state/advance.js +14 -85
  165. package/src/commands/state/approve.js +15 -97
  166. package/src/commands/state/archive.js +12 -64
  167. package/src/commands/state/auto-approval.js +2 -5
  168. package/src/commands/state/create.js +22 -109
  169. package/src/commands/state/delete.js +11 -63
  170. package/src/commands/state/finish.js +98 -475
  171. package/src/commands/state/gate-check.js +22 -132
  172. package/src/commands/state/gate-decision.js +3 -39
  173. package/src/commands/state/handoff.js +3 -30
  174. package/src/commands/state/list.js +14 -84
  175. package/src/commands/state/report-check.js +8 -58
  176. package/src/commands/state/retype.js +4 -26
  177. package/src/commands/validation/index.js +0 -3
  178. package/src/commands/validation/mutate.js +11 -11
  179. package/src/commands/validation/plan-check.js +4 -2
  180. package/src/commands/validation/tests-audit.js +7 -0
  181. package/src/commands/validation/validate-feature.js +10 -97
  182. package/src/commands/validation/verify.js +30 -113
  183. package/src/core/paths/output-schema.js +2 -101
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +0 -27
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +22 -304
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +0 -1
  191. package/src/core/work-types.js +1 -36
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  194. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +1 -5
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/main.jsx +13 -13
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  211. package/src/dashboard/app/pages/Features.jsx +1 -8
  212. package/src/dashboard/app/pages/Guide.jsx +0 -1
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +10 -69
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +0 -6
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +10 -66
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +2 -6
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/dashboard/vite.config.js +24 -24
  233. package/src/lib/ai/ai-checks.js +7 -52
  234. package/src/lib/ai/ai-project-scan.js +14 -108
  235. package/src/lib/ai/code-audit.js +7 -63
  236. package/src/lib/ai/pin-drift.js +8 -51
  237. package/src/lib/ai/registry-audit.js +11 -90
  238. package/src/lib/ci/ci-verify.js +4 -19
  239. package/src/lib/ci/github-ci.js +9 -37
  240. package/src/lib/ci/workflow-scaffold.js +14 -10
  241. package/src/lib/detectors/claude-config-detector.js +13 -74
  242. package/src/lib/detectors/index.js +4 -50
  243. package/src/lib/e2e/applicability.js +37 -0
  244. package/src/lib/e2e/compose-env.js +5 -66
  245. package/src/lib/e2e/stack-teardown.js +6 -40
  246. package/src/lib/generators/architecture-scanner.js +18 -156
  247. package/src/lib/generators/handoff-generator.js +9 -53
  248. package/src/lib/generators/scaffold-generator.js +5 -101
  249. package/src/lib/generators/settings-generator.js +4 -37
  250. package/src/lib/git/branch-classifier.js +11 -84
  251. package/src/lib/git/default-branch.js +14 -169
  252. package/src/lib/git/evidence-staleness.js +1 -0
  253. package/src/lib/git/gh.js +2 -17
  254. package/src/lib/git/pending-prs.js +6 -37
  255. package/src/lib/git/pr-files.js +3 -28
  256. package/src/lib/git/pr-index.js +3 -34
  257. package/src/lib/git/tree-fingerprint.js +3 -15
  258. package/src/lib/git/tree-state.js +6 -33
  259. package/src/lib/graph/god-nodes.js +2 -22
  260. package/src/lib/graph/graph-health.js +3 -24
  261. package/src/lib/graph/graph-loader.js +5 -56
  262. package/src/lib/graph/graphify-runtime.js +7 -37
  263. package/src/lib/graph/graphifyignore.js +8 -74
  264. package/src/lib/graph/sanitize.js +16 -136
  265. package/src/lib/heavy/cleanup.js +1 -1
  266. package/src/lib/installers/effects.js +8 -93
  267. package/src/lib/installers/mcp-installer.js +9 -169
  268. package/src/lib/installers/memory-installer.js +4 -27
  269. package/src/lib/installers/templates-reconcile.js +5 -60
  270. package/src/lib/mutation/adapters.js +6 -17
  271. package/src/lib/mutation/dotnet-introspection.js +20 -106
  272. package/src/lib/mutation/runner.js +23 -75
  273. package/src/lib/mutation/subset.js +1 -4
  274. package/src/lib/mutation/trx.js +0 -1
  275. package/src/lib/plan/artifact-vocabulary.js +5 -25
  276. package/src/lib/plan/checks.js +34 -33
  277. package/src/lib/plan/plan-parser.js +7 -8
  278. package/src/lib/projects/registry.js +7 -44
  279. package/src/lib/review-reports.js +11 -26
  280. package/src/lib/risk-detector.js +41 -173
  281. package/src/lib/specs/adr-index.js +6 -34
  282. package/src/lib/specs/frontmatter.js +2 -15
  283. package/src/lib/specs/gfm-table.js +1 -7
  284. package/src/lib/specs/revocations.js +1 -14
  285. package/src/lib/specs/screen-checklist.js +47 -124
  286. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +1 -8
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +1 -7
  291. package/src/lib/standards/sections.js +2 -13
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +21 -169
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -240
  296. package/src/lib/tasks/test-runner.js +43 -341
  297. package/src/lib/telemetry/metrics.js +7 -36
  298. package/src/lib/telemetry/telemetry-reader.js +13 -125
  299. package/src/lib/telemetry/verify-event.js +2 -30
  300. package/src/lib/tests-audit/audit.js +96 -0
  301. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  302. package/src/lib/validators/code-budget/index.js +0 -7
  303. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  304. package/src/lib/validators/contracts/index.js +0 -6
  305. package/src/lib/validators/nextjs/index.js +0 -5
  306. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  307. package/src/lib/validators/packages/index.js +0 -6
  308. package/src/lib/validators/packages/package-validator.js +6 -56
  309. package/src/lib/validators/reuse/index.js +0 -8
  310. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  311. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  312. package/src/lib/validators/traceability-check.js +5 -63
  313. package/src/lib/validators/validation-runner.js +21 -158
  314. package/src/lib/validators/verify-runner.js +85 -376
  315. package/src/lib/work-type-detector.js +5 -67
  316. package/src/lib/worktree/docker-audit.js +17 -136
  317. package/src/lib/worktree/env-allocator.js +10 -98
  318. package/src/lib/worktree/feature-truth.js +12 -115
  319. package/src/lib/worktree/finish-backup.js +32 -242
  320. package/src/lib/worktree/fleet-scanner.js +13 -95
  321. package/src/lib/worktree/ignored-triage.js +15 -91
  322. package/src/lib/worktree/vscode-settings.js +2 -3
  323. package/src/lib/worktree/worktree-audit.js +21 -213
  324. package/src/lib/worktree/worktree-include.js +3 -5
  325. package/src/scripts/global-install.js +3 -9
  326. package/src/scripts/install-plugin.js +1 -13
  327. package/src/scripts/setup-infra.js +17 -147
  328. package/src/utils/agents-installer.js +29 -276
  329. package/src/utils/banner.js +4 -25
  330. package/src/utils/claude-md-injector.js +1 -37
  331. package/src/utils/claude-settings-manager.js +1 -52
  332. package/src/utils/file-copier.js +11 -95
  333. package/src/utils/hooks-installer.js +21 -298
  334. package/src/utils/skills-installer.js +4 -101
  335. package/src/utils/version-checker.js +4 -83
  336. package/framework/commands/morph-proposal.md +0 -9
  337. package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
  338. package/framework/hooks/git/pre-commit/agents.sh +0 -25
  339. package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
  340. package/framework/hooks/git/pre-push/run-tests.sh +0 -44
  341. package/src/commands/utils/index.js +0 -3
  342. package/src/lib/detectors/design-system-detector.js +0 -188
  343. package/src/lib/generators/design-system-generator.js +0 -508
  344. package/src/lib/specs/old-format.js +0 -68
  345. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,18 +1,5 @@
1
- /**
2
- * Loop measurements over the harness event stream: turns, tool calls split by
3
- * orchestrator and sub-agent, time per command class, identical reruns, file
4
- * rereads, verify count per feature, git calls and whether the reuse
5
- * validator ran. Computed the same way on every window so a later
6
- * window compares with an earlier one.
7
- *
8
- * Every number degrades to null ("not measurable here") when the stream lacks
9
- * the field it needs — a v1 event never counts as a zero.
10
- *
11
- * A rerun is the same producer command (what runs before the output filter:
12
- * `dotnet test | tail -25` then `| tail -20` is one test run twice) in the same
13
- * feature within 10 minutes of its previous run. `rate` counts the repeats;
14
- * `burstRate` counts every run that belongs to a burst, the first included.
15
- */
1
+ // Every number degrades to null when the stream lacks its field: a v1 event never counts as a zero.
2
+ // A rerun is the same producer command (before the output filter) in the same feature within 10 minutes.
16
3
 
17
4
  import { ingestedBytes } from './telemetry-reader.js';
18
5
  import { producerOf } from '../../../framework/hooks/shared/command-tokens.js';
@@ -25,22 +12,13 @@ const READ_ONLY = new Set(['grep', 'rg', 'sed', 'ls', 'cat', 'find', 'head', 'ta
25
12
  const TOOL_EVENTS = new Set(['PostToolUse', 'PostToolUseFailure']);
26
13
  const NO_FEATURE = '\u0000';
27
14
 
28
- /**
29
- * The command without quoted text and heredoc bodies: `morph-spec verify`
30
- * inside a commit message is not a verify run.
31
- */
15
+ // `morph-spec verify` inside a commit message is not a verify run.
32
16
  function executedText(command) {
33
17
  const beforeHeredoc = command.split('<<', 1)[0];
34
18
  return beforeHeredoc.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, (q) => q[0] + ' '.repeat(q.length - 2) + q[0]);
35
19
  }
36
20
 
37
- /**
38
- * Kind (fine) and class (coarse) of a shell command. Searched anywhere in the
39
- * command, most specific first, so `cd x && dotnet test` is a test.
40
- *
41
- * @param {string} command
42
- * @returns {{ kind: string, klass: string }}
43
- */
21
+ /** Searched anywhere in the command, most specific first, so `cd x && dotnet test` is a test. */
44
22
  export function classifyCommand(command) {
45
23
  const cmd = String(command || '');
46
24
  if (/\bmorph-spec(?:\.js)?\s+verify\s+[^\s;&|)]+/.test(executedText(cmd))) return { kind: 'verify', klass: 'verify' };
@@ -120,11 +98,8 @@ function finishCounters(c, turnsObserved) {
120
98
  };
121
99
  }
122
100
 
123
- /**
124
- * Why a command ran again within the window. Edits in between and a changed
125
- * output filter need no exit code; the pass/fail split does, so a masked or v1
126
- * exit code falls back to "after an edit or not".
127
- */
101
+ // Edits in between and a changed output filter need no exit code; the pass/fail split does, so a masked
102
+ // or v1 exit code falls back to "after an edit or not".
128
103
  function rerunClass(prev, current, editsBetween) {
129
104
  if (isV2(prev) && prev.outcome === 'timeout') return 'timeout';
130
105
  if (editsBetween === 0 && prev.command !== current.command) return 'refilter';
@@ -147,11 +122,7 @@ function reuseStateOf(e) {
147
122
  return { state: 'absent', reason: null };
148
123
  }
149
124
 
150
- /**
151
- * @param {Object[]} events - deduplicated, time-ordered (loadTelemetry)
152
- * @param {{ feature?: string, top?: number }} [opts]
153
- * @returns {Object} see framework/docs/CLI.md, "cost --metrics"
154
- */
125
+ /** The returned shape is documented in framework/docs/CLI.md, "cost --metrics". */
155
126
  export function computeMetrics(events, opts = {}) {
156
127
  const top = Number.isInteger(opts.top) && opts.top > 0 ? opts.top : 10;
157
128
  const scoped = (events || []).filter((e) => e && typeof e === 'object' && (!opts.feature || e.feature === opts.feature));
@@ -1,14 +1,3 @@
1
- /**
2
- * Telemetry aggregation — reads the append-only harness event stream
3
- * (.morph/logs/events.jsonl, written by the telemetry-log hook) and summarizes
4
- * the whole execution path: which skills ran, which personas were dispatched,
5
- * which standards (knowledge base) were read, the tool mix, and the prompts.
6
- *
7
- * Pure aggregation (`aggregateTelemetry`) is separated from disk I/O
8
- * (`loadTelemetry`) so the summary logic is unit-testable without fixtures —
9
- * the same split fleet-scanner uses. CLI-side; runs in the target project cwd.
10
- */
11
-
12
1
  import { readFileSync, existsSync, readdirSync } from 'fs';
13
2
  import { join, basename } from 'path';
14
3
  import { parseWorktreeList, resolvePrimaryRoot } from '../../commands/project/worktree.js';
@@ -21,11 +10,8 @@ const EVENTS_LOG = /^events(-.+)?\.jsonl$/;
21
10
  // Caches a finish backup may hold; none of them contains a log.
22
11
  const BACKUP_SKIP_DIRS = new Set(['node_modules', '.next', 'bin', 'obj', 'TestResults', 'graphify-out', '.git']);
23
12
 
24
- /**
25
- * The same line reaches the reader through the live log, its rotated archives
26
- * and every finish backup that copied it. v2 events carry an `eventId`; a v1
27
- * event is identical only to a byte-identical copy of itself.
28
- */
13
+ // The same line arrives through the live log, its rotated archives and every finish backup. v2 events
14
+ // carry an `eventId`; a v1 event is identical only to a byte-identical copy of itself.
29
15
  function dedupeKey(event) {
30
16
  return event.eventId ? `id:${event.eventId}` : `raw:${JSON.stringify(event)}`;
31
17
  }
@@ -35,13 +21,7 @@ function eventTime(event) {
35
21
  return Number.isNaN(t) ? 0 : t;
36
22
  }
37
23
 
38
- /**
39
- * Collapse copies of the same event and restore time order across sources.
40
- * The sort is stable, so events sharing a timestamp keep their write order.
41
- *
42
- * @param {Object[]} events
43
- * @returns {Object[]}
44
- */
24
+ /** Stable sort, so events sharing a timestamp keep their write order. */
45
25
  export function dedupeEvents(events) {
46
26
  const seen = new Set();
47
27
  const out = [];
@@ -61,9 +41,9 @@ function readJsonlFile(path, out) {
61
41
  for (const line of readFileSync(path, 'utf-8').split('\n')) {
62
42
  const trimmed = line.trim();
63
43
  if (!trimmed) continue;
64
- try { out.push(JSON.parse(trimmed)); } catch { /* skip corrupt line */ }
44
+ try { out.push(JSON.parse(trimmed)); } catch {}
65
45
  }
66
- } catch { /* unreadable file contributes nothing */ }
46
+ } catch {}
67
47
  }
68
48
 
69
49
  function archiveFiles(sessionsDir) {
@@ -77,15 +57,7 @@ function archiveFiles(sessionsDir) {
77
57
  }
78
58
  }
79
59
 
80
- /**
81
- * Read the event stream of a project root: the live log plus, by default, the
82
- * archives SessionStart rotated into `.morph/logs/sessions/` — reading only the
83
- * live file loses everything older than the last 5 MB.
84
- *
85
- * @param {string} [projectPath]
86
- * @param {{ archives?: boolean }} [options]
87
- * @returns {Object[]} deduplicated, in time order; [] when there is no log
88
- */
60
+ /** Live log plus, by default, the rotated archives: the live file alone loses everything past 5 MB. */
89
61
  export function loadTelemetry(projectPath, { archives = true } = {}) {
90
62
  const root = projectPath || process.cwd();
91
63
  const events = [];
@@ -108,14 +80,7 @@ function collectBackupLogs(dir, depth, out) {
108
80
  }
109
81
  }
110
82
 
111
- /**
112
- * Event logs that `finish` saved under `<git-common-dir>/morph-spec/finish-backup/`
113
- * before tearing a worktree down — once the worktree is gone, a feature built
114
- * there leaves its telemetry nowhere else.
115
- *
116
- * @param {string} [cwd]
117
- * @returns {{ path: string, events: Object[] }|null}
118
- */
83
+ /** Logs `finish` saved before tearing a worktree down: once it is gone, they are the feature's only telemetry. */
119
84
  export function loadFinishBackupTelemetry(cwd = process.cwd()) {
120
85
  const common = resolveGitCommonDir(cwd);
121
86
  if (!common) return null;
@@ -128,19 +93,7 @@ export function loadFinishBackupTelemetry(cwd = process.cwd()) {
128
93
  return { path: base, events: dedupeEvents(events) };
129
94
  }
130
95
 
131
- /**
132
- * Every git worktree that has its OWN `.morph/logs/events.jsonl` — the source
133
- * of `cost --all-trees` — plus a `finish-backup` entry for worktrees already
134
- * torn down. Reuses the worktree enumeration of `doctor --worktrees`/`fleet`.
135
- *
136
- * The PRIMARY worktree is labeled `'root'`; every other entry by its directory
137
- * basename (typically the feature slug). A tree that never ran the harness
138
- * contributes nothing. Outside a git repo this degrades to a single root
139
- * entry — never throws.
140
- *
141
- * @param {string} [cwd]
142
- * @returns {Array<{ tree: string, path: string, events: Object[] }>}
143
- */
96
+ /** Labels the primary worktree `'root'`, the others by directory basename. Outside git, a single root entry. */
144
97
  export function loadTelemetryAllTrees(cwd = process.cwd()) {
145
98
  const root = resolvePrimaryRoot(cwd);
146
99
  const entries = parseWorktreeList(root);
@@ -167,21 +120,6 @@ function rank(map) {
167
120
  .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
168
121
  }
169
122
 
170
- /**
171
- * Summarize an event stream.
172
- *
173
- * @param {Object[]} events - raw events (from loadTelemetry)
174
- * @param {{ feature?: string }} [opts] - optional feature filter
175
- * @returns {{
176
- * total: number,
177
- * skills: Array<{name:string,count:number}>,
178
- * personas: Array<{name:string,count:number}>,
179
- * standards: Array<{path:string,count:number}>,
180
- * tools: Array<{name:string,count:number}>,
181
- * prompts: Array<{ts:string,source:'user'|'agent',feature:?string,phase:?string,text:string}>,
182
- * features: string[]
183
- * }}
184
- */
185
123
  export function aggregateTelemetry(events, opts = {}) {
186
124
  const scoped = opts.feature
187
125
  ? (events || []).filter(e => e.feature === opts.feature)
@@ -226,21 +164,8 @@ export function aggregateTelemetry(events, opts = {}) {
226
164
  };
227
165
  }
228
166
 
229
- // ─────────────────────────────────────────────────────────────────────────
230
- // Cost aggregation — sibling to aggregateTelemetry (kept separate rather than
231
- // folded in: aggregateTelemetry's return shape is pinned by
232
- // test/commands/telemetry.test.js, which spreads it wholesale into --json
233
- // output — adding fields there would leak into that contract).
234
- //
235
- // Turns the same raw event stream into a cost-facing view: how many context
236
- // bytes were re-ingested (by kind, globally and per phase), and how many
237
- // sub-agent dispatches happened (and how many prompt chars they cost).
238
- //
239
- // "Bytes re-ingested" is a DETERMINISTIC PROXY for cost, not a token count —
240
- // hooks can only see what tools read off disk, never the model's actual
241
- // token usage. The `cost` command surfaces that caveat; this module only
242
- // computes the numbers.
243
- // ─────────────────────────────────────────────────────────────────────────
167
+ // Kept apart from aggregateTelemetry: its return shape is spread into `telemetry --json`, so new fields
168
+ // there would leak into that contract. Bytes re-ingested is a proxy for cost, never a token count.
244
169
 
245
170
  function mapToObj(map) {
246
171
  const out = {};
@@ -254,45 +179,8 @@ function sumValues(map) {
254
179
  return total;
255
180
  }
256
181
 
257
- /**
258
- * Summarize an event stream for cost purposes: bytes-re-ingested buckets by
259
- * `kind` (global and per phase) and sub-agent dispatch counts.
260
- *
261
- * Byte attribution prefers `bytesRead` (the line window a Read put in context)
262
- * and falls back to `bytes` (the file size — all v1 events have) — keyed off
263
- * the number alone, not the tool.
264
- *
265
- * Dispatch prompt size (Furo 3 — contract with the hooks-side telemetry-log
266
- * rewrite): a dispatch event's `prompt` field is now TRUNCATED to 2k chars on
267
- * write, with the original length carried separately as `promptChars`. When
268
- * `promptChars` (a number) is present it is preferred; a legacy stream that
269
- * only ever had the untruncated `prompt` string falls back to
270
- * `prompt.length`, unchanged from before — every existing byte count stays
271
- * exact against old logs.
272
- *
273
- * SessionUsage (main session) and AgentUsage (each sub-agent) are real token
274
- * counts, not the bytes proxy, and never touch toolCounts/bytesByKind/
275
- * dispatches. Only their `delta` is summed — the top-level numbers are a
276
- * running total repeated on every sample — so a v1 sample (no `delta`) is
277
- * counted in `sessionUsageLegacySamples` and never added in. Like any event
278
- * they obey the `--feature` filter. `sessionUsage`/`agentUsage` are `null`
279
- * when nothing summable was observed, never a zeroed object.
280
- *
281
- * @param {Object[]} events - raw events (from loadTelemetry)
282
- * @param {{ feature?: string }} [opts] - optional feature filter
283
- * @returns {{
284
- * total: number,
285
- * features: string[],
286
- * bytesByKind: Object<string, number>,
287
- * bytesTotal: number,
288
- * dispatches: {count:number, promptChars:number},
289
- * sessionUsage: {inputTokens:number, outputTokens:number, cacheReadTokens:number, cacheCreationTokens:number, sessions:number, samples:number}|null,
290
- * sessionUsageLegacySamples: number,
291
- * agentUsage: {inputTokens:number, outputTokens:number, cacheReadTokens:number, cacheCreationTokens:number, agents:number, samples:number}|null,
292
- * tools: Array<{name:string,count:number}>,
293
- * phases: Array<{phase:string, bytesByKind:Object<string,number>, bytesTotal:number}>,
294
- * }}
295
- */
182
+ // Prefers `promptChars` over `prompt.length`: the prompt is truncated on write. Session and agent usage sum
183
+ // only their `delta`, never the running total; they are `null` when nothing summable was seen.
296
184
  const SESSION_USAGE_KEYS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheCreationTokens'];
297
185
 
298
186
  /** Bytes a Read put in context: its window when known, else the file size. */
@@ -312,7 +200,7 @@ export function aggregateCost(events, opts = {}) {
312
200
  const toolCounts = new Map();
313
201
  const bytesByKindGlobal = new Map();
314
202
  const dispatches = { count: 0, promptChars: 0 };
315
- const phaseBytes = new Map(); // phase -> Map(kind -> bytes)
203
+ const phaseBytes = new Map();
316
204
  const newUsageBucket = () => ({
317
205
  totals: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 },
318
206
  ids: new Set(),
@@ -1,22 +1,9 @@
1
- /**
2
- * The `Verify` telemetry event: one line per `morph-spec verify` run with the
3
- * per-node status and duration, the commit it covered and whether the
4
- * reuse validator could run. The Bash event of the same command says who
5
- * called it and how long the whole command took; this one says what it did.
6
- */
7
-
8
1
  import { existsSync } from 'fs';
9
2
  import { join } from 'path';
10
3
  import { appendTelemetryEvent, readTelemetryEvents } from '../../../framework/hooks/shared/telemetry-logger.js';
11
4
 
12
5
  const NODE_NAMES = ['build', 'tests', 'validators', 'e2e'];
13
6
 
14
- /**
15
- * @param {Object} result - runVerify() output
16
- * @param {{ sha: string|null, tree: 'root'|'worktree', persisted: boolean, reason: string|null,
17
- * fingerprint?: string|null, source?: 'local'|'ci' }} context
18
- * @returns {Object}
19
- */
20
7
  export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerprint = null, source = 'local' }) {
21
8
  const nodes = {};
22
9
  for (const name of NODE_NAMES) {
@@ -43,16 +30,7 @@ export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerp
43
30
  };
44
31
  }
45
32
 
46
- /**
47
- * The last complete run of the same feature's verify over the
48
- * same tree, or null. A partial run (`--filter`, `--skip-*`, `--no-persist`)
49
- * is not the proof a complete one replaces, so it is passed over. Only the
50
- * live log is read: a repeat worth warning about is recent.
51
- *
52
- * @param {string} projectPath
53
- * @param {{ feature: string, fingerprint: string|null }} run
54
- * @returns {{ ts: string, status: string, sha: string|null }|null}
55
- */
33
+ /** A partial run (`--filter`, `--skip-*`, `--no-persist`) is not the proof a complete one replaces. */
56
34
  export function findRepeatedVerify(projectPath, { feature, fingerprint }) {
57
35
  if (!fingerprint) return null;
58
36
  const events = readTelemetryEvents(projectPath);
@@ -65,13 +43,7 @@ export function findRepeatedVerify(projectPath, { feature, fingerprint }) {
65
43
  return null;
66
44
  }
67
45
 
68
- /**
69
- * Appends only where a morph project already exists: this command must never
70
- * be the one that creates `.morph/`.
71
- *
72
- * @param {string} projectPath
73
- * @param {Object} event
74
- */
46
+ // Appends only where a morph project exists: this command must never be the one that creates `.morph/`.
75
47
  export function recordVerifyEvent(projectPath, event) {
76
48
  if (!existsSync(join(projectPath, '.morph'))) return;
77
49
  appendTelemetryEvent(event, projectPath);
@@ -0,0 +1,96 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { readFileSync } from 'fs';
3
+ import { basename, join } from 'path';
4
+ import { isTestPath } from '../graph/sanitize.js';
5
+
6
+ export const PROSE_MIN_HITS = 3;
7
+ export const FAMILY_MIN_SIZE = 4;
8
+
9
+ const TEST_FILE = /\.(cs|[cm]?[jt]sx?)$/i;
10
+ const CS_CASE = /\[\s*\w*(Fact|Theory)\b/g;
11
+ const JS_CASE = /^\s*(?:it|test)(?:\.each\s*\([\s\S]*?\))?\s*\(\s*['"`]/gm;
12
+ const READS_DOC = /(ReadAll\w*|readFileSync)\s*\([^)]*\.(md|txt)\b/;
13
+ const PROSE_ASSERT = /(?:Assert\.(?:Contains|DoesNotContain)\(\s*"|\.(?:toContain|toMatch)\(\s*['"`])([^"'`]{20,})/g;
14
+ const CONSTANT_ASSERT = /Assert\.Equal\(\s*"[^"]*"\s*,\s*[A-Z]\w*(?:\.[A-Z]\w*)+\s*\)|expect\(\s*[A-Z][A-Z0-9_]+(?:\.[A-Z0-9_]+)*\s*\)\.toBe\(\s*['"`]/g;
15
+ const CS_METHOD = /\[\s*\w*Fact\b[^\]]*\][\s\S]*?(?:void|Task)\s+(\w+)\s*\(/g;
16
+ const LAYER = /^(\w+?)(Handler|Endpoints?|EndpointIntegration|Integration)Tests$/;
17
+
18
+ function trackedTestFiles(root) {
19
+ try {
20
+ return execFileSync('git', ['ls-files', '-z'], { cwd: root, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 256 * 1024 * 1024 })
21
+ .toString().split('\0').filter((f) => f && TEST_FILE.test(f) && isTestPath(f));
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ const lineOf = (text, index) => text.slice(0, index).split('\n').length;
28
+ const countOf = (text, re) => (text.match(re) ?? []).length;
29
+
30
+ function factFamilies(text) {
31
+ const byStem = new Map();
32
+ for (const m of text.matchAll(CS_METHOD)) {
33
+ const name = m[1];
34
+ const cut = name.lastIndexOf('_');
35
+ if (cut <= 0) continue;
36
+ const stem = name.slice(0, cut);
37
+ byStem.set(stem, [...(byStem.get(stem) ?? []), name]);
38
+ }
39
+ return [...byStem].filter(([, names]) => names.length >= FAMILY_MIN_SIZE).map(([stem, names]) => ({ stem, size: names.length }));
40
+ }
41
+
42
+ /** Read-only heuristics over the tracked test files: candidates to delete or merge, never verdicts. */
43
+ export function auditTests(root) {
44
+ const files = trackedTestFiles(root);
45
+ if (files === null) return { ok: false, reason: 'git ls-files não respondeu — a auditoria lê só arquivos rastreados' };
46
+
47
+ const perFile = [];
48
+ const prose = [];
49
+ const constants = [];
50
+ const families = [];
51
+ const layers = new Map();
52
+ for (const file of files) {
53
+ let text;
54
+ try { text = readFileSync(join(root, file), 'utf8'); } catch { continue; }
55
+ const cases = /\.cs$/i.test(file) ? countOf(text, CS_CASE) : countOf(text, JS_CASE);
56
+ if (cases === 0) continue;
57
+ perFile.push({ file, cases });
58
+
59
+ const proseHits = [...text.matchAll(PROSE_ASSERT)].filter((m) => /\s/.test(m[1]));
60
+ if (READS_DOC.test(text) || proseHits.length >= PROSE_MIN_HITS) {
61
+ prose.push({ file, readsDoc: READS_DOC.test(text), literalAsserts: proseHits.length });
62
+ }
63
+ for (const m of text.matchAll(CONSTANT_ASSERT)) constants.push({ file, line: lineOf(text, m.index) });
64
+ for (const family of factFamilies(text)) families.push({ file, ...family });
65
+
66
+ const layer = LAYER.exec(basename(file).replace(/\.\w+$/, ''));
67
+ if (layer) layers.set(layer[1], [...(layers.get(layer[1]) ?? []), file]);
68
+ }
69
+
70
+ perFile.sort((a, b) => b.cases - a.cases);
71
+ return {
72
+ ok: true,
73
+ files: perFile.length,
74
+ cases: perFile.reduce((n, f) => n + f.cases, 0),
75
+ largest: perFile.slice(0, 10),
76
+ prose,
77
+ constants,
78
+ families,
79
+ layers: [...layers].filter(([, f]) => f.length > 1).map(([subject, f]) => ({ subject, files: f })),
80
+ };
81
+ }
82
+
83
+ export function renderTestsAudit(result) {
84
+ if (!result.ok) return `tests audit: ${result.reason}`;
85
+ const out = [`${result.cases} casos em ${result.files} arquivos de teste`, ''];
86
+ const section = (title, rows) => {
87
+ if (rows.length === 0) return;
88
+ out.push(`${title} (${rows.length})`, ...rows.map((r) => ` ${r}`), '');
89
+ };
90
+ section('Maiores arquivos', result.largest.map((f) => `${f.cases} ${f.file}`));
91
+ section('Fixam texto ou leem doc — candidatos a apagar', result.prose.map((p) => `${p.file}${p.readsDoc ? ' (lê .md/.txt)' : ''}${p.literalAsserts ? ` — ${p.literalAsserts} asserts de frase` : ''}`));
92
+ section('Constante contra o próprio literal — candidatos a apagar', result.constants.map((c) => `${c.file}:${c.line}`));
93
+ section(`Famílias de [Fact] (≥ ${FAMILY_MIN_SIZE} com o mesmo prefixo) — candidatas a [Theory]`, result.families.map((f) => `${f.file} — ${f.stem}_* ×${f.size}`));
94
+ section('Mesmo assunto em mais de uma camada — mantenha a mais barata que morde', result.layers.map((l) => `${l.subject}: ${l.files.join(', ')}`));
95
+ return out.join('\n').trimEnd();
96
+ }
@@ -1,20 +1,4 @@
1
- /**
2
- * Code Budget Validator — o que a feature adicionou além do código.
3
- *
4
- * Mede, só nas linhas que esta árvore adicionou contra a branch default:
5
- * (a) linhas de comentário que citam processo (task, UC, ADR, Gate...), que o
6
- * standard `architecture-code-comments` manda para o commit;
7
- * (b) linhas de teste ÷ linhas de produção, contra o orçamento por task do
8
- * `morph-plan` (um teste por pós-condição + um de regressão por bug);
9
- * (c) linhas de comentário ÷ linhas de código, porque o padrão do standard é
10
- * nenhum comentário. Doc-comment (`///`, JSDoc) conta como qualquer outro.
11
- *
12
- * Sempre `warning`: é medida de volume, e o excesso pode estar justificado na
13
- * task. Um aviso por medida, não por linha, porque cada aviso custa 1 ponto no
14
- * score do validador. Qualquer falha de medição vira skip com razão.
15
- *
16
- * @module code-budget-validator
17
- */
1
+ // Sempre `warning`: é medida de volume, e o excesso pode estar justificado no plano.
18
2
 
19
3
  import { readFileSync } from 'fs';
20
4
  import { join } from 'path';
@@ -25,31 +9,21 @@ import { isTestFile } from '../reuse/symbol-scanner.js';
25
9
  import { isGenerated, isProjectSource } from '../../../../framework/hooks/shared/source-files.js';
26
10
  import { citesProcess, commentLines } from '../../../../framework/hooks/shared/comment-scanner.js';
27
11
 
28
- /** Razão teste ÷ produção acima da qual se avisa. */
29
- export const MAX_TEST_RATIO = 1.5;
12
+ export const MAX_TEST_RATIO = 1;
30
13
 
31
14
  /** Abaixo disto a razão não é julgada: a regressão de um bug de 2 linhas cabe aqui. */
32
15
  export const MIN_TEST_LINES = 40;
33
16
 
34
- /** Mais de uma linha de comentário a cada dez de código: os nomes não estão fazendo o trabalho. */
35
17
  export const MAX_COMMENT_RATIO = 0.1;
36
18
 
37
19
  /** Abaixo disto a densidade não é julgada: um comentário legítimo num arquivo pequeno já passaria da razão. */
38
20
  export const MIN_CODE_LINES = 40;
39
21
 
40
- /** Quantos locais citar no aviso de comentário antes de resumir com "(+N)". */
41
22
  const MAX_CITED = 4;
42
23
 
43
24
  const decimal = (n) => n.toFixed(1).replace('.', ',');
44
25
 
45
- /**
46
- * Puro, sem I/O.
47
- *
48
- * @param {Array<{path: string, content: string, added: number[]|null}>} files -
49
- * `added` são as linhas adicionadas; `null` é o arquivo inteiro (novo)
50
- * @returns {{commentLines: number, processLines: Array<{file: string, line: number, text: string}>,
51
- * testLines: number, productionLines: number, commentsByFile: Record<string, number>}}
52
- */
26
+ /** `added` são as linhas adicionadas de cada arquivo; `null` é o arquivo inteiro (novo). */
53
27
  export function measureCodeBudget(files) {
54
28
  const measured = { commentLines: 0, processLines: [], testLines: 0, productionLines: 0, commentsByFile: {} };
55
29
 
@@ -74,10 +48,6 @@ export function measureCodeBudget(files) {
74
48
  return measured;
75
49
  }
76
50
 
77
- /**
78
- * @param {ReturnType<typeof measureCodeBudget>} measured
79
- * @returns {Array<object>} avisos no formato do validation-runner
80
- */
81
51
  export function budgetIssues(measured) {
82
52
  const issues = [];
83
53
  const { commentLines: comments, processLines, testLines, productionLines, commentsByFile = {} } = measured;
@@ -93,7 +63,7 @@ export function budgetIssues(measured) {
93
63
  file: processLines[0].file,
94
64
  line: processLines[0].line,
95
65
  solution:
96
- 'A história vai na mensagem de commit e no decisions.md; no código fica só o porquê que ' +
66
+ 'A história vai na mensagem de commit e no plan.md; no código fica só o porquê que ' +
97
67
  'ele não mostra (standard architecture-code-comments).',
98
68
  });
99
69
  }
@@ -133,16 +103,6 @@ export function budgetIssues(measured) {
133
103
  return issues;
134
104
  }
135
105
 
136
- /**
137
- * Validador plugado no `validation-runner` (e por ele no nó `validators` do
138
- * `morph-spec verify`).
139
- *
140
- * @param {string} projectPath
141
- * @param {string|null} [featureName] - não usado: a medida é da árvore inteira contra a default
142
- * @param {object} [options]
143
- * @param {Map<string, number[]|null>} [options.addedLines] - override da coleta (testes)
144
- * @returns {Promise<{errors: number, warnings: number, issues: object[], _skipReason?: string}>}
145
- */
146
106
  export async function validateCodeBudget(projectPath, featureName = null, options = {}) {
147
107
  const result = { errors: 0, warnings: 0, issues: [] };
148
108
 
@@ -1,9 +1,2 @@
1
- /**
2
- * Code Budget Validators
3
- *
4
- * Volume que a feature adicionou além do código: comentário que conta a
5
- * história do processo e teste acima do orçamento (ver code-budget-validator.js).
6
- */
7
-
8
1
  export * from './code-budget-validator.js';
9
2
  export * from '../../../../framework/hooks/shared/comment-scanner.js';
@@ -18,7 +18,6 @@ export async function validateContracts(projectPath, featureName, options = {})
18
18
  return skipped('plan.md declares no interfaces/records/enums — skipping');
19
19
  }
20
20
 
21
- // Find all .cs files in the project (excluding .morph outputs, bin, obj)
22
21
  const csFiles = await glob('**/*.cs', {
23
22
  cwd: projectPath,
24
23
  ignore: ['.morph/**', '**/bin/**', '**/obj/**', '**/node_modules/**']
@@ -30,7 +29,6 @@ export async function validateContracts(projectPath, featureName, options = {})
30
29
  try {
31
30
  projectCode[file] = readFileSync(fullPath, 'utf-8');
32
31
  } catch {
33
- // Skip unreadable files
34
32
  }
35
33
  }
36
34
 
@@ -39,7 +37,6 @@ export async function validateContracts(projectPath, featureName, options = {})
39
37
  let total = 0;
40
38
  const interfaceResults = [];
41
39
 
42
- // Validate interfaces
43
40
  for (const iface of contracts.interfaces) {
44
41
  total++;
45
42
  const result = validateInterface(iface, projectCode, options);
@@ -58,7 +55,6 @@ export async function validateContracts(projectPath, featureName, options = {})
58
55
  });
59
56
  }
60
57
 
61
- // Check for missing methods
62
58
  for (const method of result.missingMethods || []) {
63
59
  issues.push({
64
60
  level: 'error',
@@ -69,7 +65,6 @@ export async function validateContracts(projectPath, featureName, options = {})
69
65
  }
70
66
  }
71
67
 
72
- // Validate records/DTOs
73
68
  for (const record of contracts.records) {
74
69
  total++;
75
70
  const found = Object.entries(projectCode).some(([, content]) =>
@@ -90,7 +85,6 @@ export async function validateContracts(projectPath, featureName, options = {})
90
85
  }
91
86
  }
92
87
 
93
- // Validate enums
94
88
  for (const enumDef of contracts.enums) {
95
89
  total++;
96
90
  const found = Object.entries(projectCode).some(([, content]) =>
@@ -138,7 +132,6 @@ function parseContracts(content) {
138
132
  const records = [];
139
133
  const enums = [];
140
134
 
141
- // Extract interfaces with their methods
142
135
  const interfaceRegex = /public\s+interface\s+(I\w+)(?:\s*:\s*[\w<>,\s]+)?\s*\{([^}]*)\}/gs;
143
136
  let match;
144
137
 
@@ -150,18 +143,15 @@ function parseContracts(content) {
150
143
  interfaces.push({ name, methods });
151
144
  }
152
145
 
153
- // Extract records/classes (DTOs)
154
146
  const recordRegex = /public\s+(record|class|struct)\s+(\w+)/g;
155
147
  while ((match = recordRegex.exec(content)) !== null) {
156
148
  const type = match[1];
157
149
  const name = match[2];
158
- // Skip interface names that were already captured
159
150
  if (!name.startsWith('I') || !interfaces.some(i => i.name === name)) {
160
151
  records.push({ type, name });
161
152
  }
162
153
  }
163
154
 
164
- // Extract enums
165
155
  const enumRegex = /public\s+enum\s+(\w+)/g;
166
156
  while ((match = enumRegex.exec(content)) !== null) {
167
157
  enums.push({ name: match[1] });
@@ -170,9 +160,6 @@ function parseContracts(content) {
170
160
  return { interfaces, records, enums };
171
161
  }
172
162
 
173
- /**
174
- * Extract method signatures from an interface body
175
- */
176
163
  function extractMethods(body) {
177
164
  const methods = [];
178
165
 
@@ -182,7 +169,6 @@ function extractMethods(body) {
182
169
 
183
170
  while ((match = methodRegex.exec(body)) !== null) {
184
171
  const methodName = match[1];
185
- // Skip property getters/setters
186
172
  if (!['get', 'set', 'init'].includes(methodName)) {
187
173
  methods.push(methodName);
188
174
  }
@@ -191,9 +177,6 @@ function extractMethods(body) {
191
177
  return methods;
192
178
  }
193
179
 
194
- /**
195
- * Validate that an interface is implemented in project code
196
- */
197
180
  function validateInterface(iface, projectCode, options) {
198
181
  const result = {
199
182
  name: iface.name,
@@ -205,7 +188,6 @@ function validateInterface(iface, projectCode, options) {
205
188
  missingMethods: []
206
189
  };
207
190
 
208
- // Search for classes implementing this interface
209
191
  const implRegex = new RegExp(`class\\s+(\\w+)\\s*(?:<[^>]*>)?\\s*:\\s*[^{]*${escapeRegex(iface.name)}`, 'g');
210
192
 
211
193
  for (const [file, content] of Object.entries(projectCode)) {
@@ -215,7 +197,6 @@ function validateInterface(iface, projectCode, options) {
215
197
  result.implementingClass = match[1];
216
198
  result.implementingFile = file;
217
199
 
218
- // Check which methods are implemented
219
200
  for (const method of iface.methods) {
220
201
  const methodExists = new RegExp(`(public|protected|private|internal)?\\s*(async\\s+)?\\S+\\s+${escapeRegex(method)}\\s*\\(`, 'g').test(content);
221
202
  if (methodExists) {
@@ -225,17 +206,14 @@ function validateInterface(iface, projectCode, options) {
225
206
  }
226
207
  }
227
208
 
228
- break; // Found the implementation
209
+ break;
229
210
  }
230
- implRegex.lastIndex = 0; // Reset for next file
211
+ implRegex.lastIndex = 0;
231
212
  }
232
213
 
233
214
  return result;
234
215
  }
235
216
 
236
- /**
237
- * Escape special regex characters in a string
238
- */
239
217
  function escapeRegex(str) {
240
218
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
241
219
  }