@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,106 +1,29 @@
1
- /**
2
- * src/lib/standards/verified-against.js
3
- *
4
- * THE parser for a standard's verification header and for its age against the
5
- * AI package pin. Single source of truth, on purpose.
6
- *
7
- * A standard declares what it was checked against, and when:
8
- *
9
- * **Verified against:** Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08);
10
- * provado por `templates/dotnet/ai-kit/.../AgentFactory.cs`. Last-verified: 2026-09-08.
11
- *
12
- * Three places need to read that same fact — `scripts/check-freshness.js`
13
- * (the freshness gate), `test/framework/standard-header.test.js` (the header
14
- * contract) and the "ai-agents vs ai-pin" report. Three copies of the regex
15
- * would drift apart, which is exactly the failure mode CLAUDE.md's Dependency
16
- * Matrix exists to catalogue. So the regex lives here, once.
17
- *
18
- * The age is measured against `framework/ai-pin.json`'s `pinnedAt`, never
19
- * against "today": `pinnedAt` is the date a human checked the packages, so the
20
- * obligation is tied to a human act. Measuring against today would make a
21
- * parked repository change colour on its own.
22
- *
23
- * NOTE on the 90-day window: this module EXPOSES the age and the threshold; it
24
- * enforces nothing — enforcement lives in its consumers. Issue #93 re-verified
25
- * the `ai-agents` standards and turned the window into a HARD FAIL in
26
- * scripts/check-freshness.js, so the number below is no longer only a warning.
27
- * The name kept the `WARN` in it because renaming an export is a change to the
28
- * module's surface, not to its documentation; scripts/check-freshness.js aliases
29
- * it to `AI_PIN_STALE_DAYS` rather than re-declaring `90` — see that file's
30
- * docblock and CONTRIBUTING.md's "two rulers" table.
31
- */
1
+ // Age is measured against `pinnedAt` of framework/ai-pin.json, never "today":
2
+ // a parked repository must not change colour on its own.
32
3
 
33
4
  import { readFileSync, existsSync } from 'fs';
34
5
  import { join, dirname } from 'path';
35
6
  import { fileURLToPath } from 'url';
36
7
 
37
- /** Repository root, resolved from this file: src/lib/standards → ../../.. */
38
8
  const DEFAULT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
39
9
 
40
- /** Path of the pin relative to a root. */
41
10
  const AI_PIN_RELATIVE = join('framework', 'ai-pin.json');
42
11
 
43
- /**
44
- * The verification header, on ONE line.
45
- *
46
- * Group 1 is the subject (what the standard was checked against), group 2 the
47
- * ISO date. `[^\n]*?` keeps the match inside a single line: a `Last-verified:`
48
- * two paragraphs below belongs to another claim, and stitching the two would
49
- * date a header that never carried a date.
50
- *
51
- * @type {RegExp}
52
- */
12
+ // Group 1 = subject, group 2 = ISO date. `[^\n]*?` keeps it on one line: a
13
+ // `Last-verified:` further down belongs to another claim.
53
14
  export const LAST_VERIFIED_RE = /\*\*Verified against:\*\*([^\n]*?)Last-verified:\s*(\d{4}-\d{2}-\d{2})/;
54
15
 
55
- /**
56
- * An unresolved `// VERIFY` marker: a code detail the author could not confirm
57
- * against the installed package.
58
- *
59
- * Deliberately broader than the regex it replaces (`/^\s*\/\/\s*VERIFY:/`),
60
- * which demanded line-start AND a colon. Every real marker in
61
- * `framework/standards/ai-agents/` is an end-of-line comment without a colon —
62
- *
63
- * using OpenAI.Batch; // VERIFY namespace — confirme contra o pacote OpenAI 2.*
64
- *
65
- * — so the old counter reported 0 while two standards openly declared
66
- * unconfirmed C# API. `\b` keeps `// VERIFYING` out; the fence requirement is
67
- * NOT in the regex (a regex cannot see a fence) — it lives in
68
- * {@link countVerifyMarkers}, so prose quoting `// VERIFY:` never counts.
69
- *
70
- * @type {RegExp}
71
- */
16
+ // Real markers are end-of-line and often lack a colon (`x; // VERIFY namespace`),
17
+ // so no line-start/colon anchor. The fence rule lives in countVerifyMarkers.
72
18
  export const VERIFY_MARKER_RE = /(^|\s)\/\/\s*VERIFY\b/;
73
19
 
74
- /**
75
- * Age, in days against `pinnedAt`, above which an `ai-agents/**` standard is
76
- * due for re-verification.
77
- *
78
- * READ UNDER TWO NAMES, ONE VALUE. `scripts/check-freshness.js` imports it as
79
- * `AI_PIN_STALE_DAYS` and **hard-fails** the run above it (issue #93), for
80
- * standards under `framework/standards/ai-agents/` measured against `pinnedAt`.
81
- * The alias exists so the number is declared once: a second literal `90` is the
82
- * same drift that once blinded the `// VERIFY` counter.
83
- *
84
- * The `WARN` in the name is now historical — it shipped in issue #92, when the
85
- * window was a measurement. Kept because renaming an export changes the module's
86
- * surface; this comment changes only what the reader is told.
87
- *
88
- * @type {number}
89
- */
20
+ // scripts/check-freshness.js hard-fails above this (aliased as AI_PIN_STALE_DAYS):
21
+ // declare the number only here.
90
22
  export const AI_AGENTS_WARN_DAYS = 90;
91
23
 
92
- /** ISO calendar date, the only date shape this module accepts. */
93
24
  const ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
94
25
 
95
- /**
96
- * Parse a standard's verification header.
97
- *
98
- * @param {string} text - Full markdown text of the standard.
99
- * @returns {{ verifiedAgainst: string, lastVerified: string }|null}
100
- * `null` when the standard carries no parseable header — the caller decides
101
- * whether that is a warning (the backfill backlog) or a failure
102
- * (`ai-agents/**`, where a new standard must be born dated).
103
- */
26
+ /** `null` when no parseable header: the caller decides if that is a warning or a failure. */
104
27
  export function parseVerifiedAgainst(text) {
105
28
  if (typeof text !== 'string' || text.length === 0) return null;
106
29
  const m = LAST_VERIFIED_RE.exec(text);
@@ -108,80 +31,18 @@ export function parseVerifiedAgainst(text) {
108
31
  return { verifiedAgainst: m[1].trim(), lastVerified: m[2] };
109
32
  }
110
33
 
111
- /**
112
- * The `provado por` clause, and the path token that may follow it.
113
- *
114
- * THREE TYPESETTINGS EXIST IN THE ACERVO, and all three are real claims.
115
- * Counted by re-deriving every one of the 78 standards on disk and summing the
116
- * resulting paths — 27 paths across 16 entries:
117
- *
118
- * A ; provado por `templates/…/AgentFactory.cs` e `.../ConversationStore.cs`
119
- * B ; `provado por templates/…/AgentFactory.cs` para a **ordem do pipeline**
120
- * C . O timeout é **provado por** `.../AgentSpecOptions.cs` e `.../AgentFactory.cs`
121
- *
122
- * B opens the code span BEFORE the clause; C wraps the clause in bold and can
123
- * appear MID-SENTENCE, after an earlier clause already closed. Each one was
124
- * found by measuring, and each was missing from the rule that preceded it:
125
- *
126
- * - ignoring B dropped 4 of 16 claims (`middleware-patterns`,
127
- * `observability-patterns`, `rag-custom-pgvector`, `structured-output`);
128
- * - ignoring C dropped 2 paths from `llm-runtime-defaults` — 25 instead of 27.
129
- *
130
- * READ THE SHAPE OF THAT SECOND MISS, because it is why counting entries is not
131
- * enough: `llm-runtime-defaults` had two OTHER paths from a clause of form A, so
132
- * it stayed in the "16 entries with proof" bucket either way. No counter moved,
133
- * `--check` stayed green, exit 0. The only ruler that saw it is the SUM of
134
- * paths. A canary that pins entry counts would have gone green over it — which
135
- * is why `standards-registry.test.js` pins the paths themselves, written out by
136
- * hand, and not a total produced by this function.
137
- *
138
- * The DENIALS must stay empty, and they are the majority (61 of the 77 headers
139
- * derive to `[]`; 16 say some variant of "sem cláusula `provado por`" or
140
- * "NÃO carimba `provado por`"). Three properties keep them out without a
141
- * blacklist of prose:
142
- *
143
- * 1. a path must contain a `/` — so "`provado por`. Os nomes" and
144
- * "`provado por`: o ai-kit" match nothing;
145
- * 2. a path must be CLOSED BY A BACKTICK — so prose that merely follows the
146
- * clause ("para a **ordem do pipeline**") cannot be mistaken for a path;
147
- * 3. the clause must not be the tail of a LONGER word — `aprovado por` and
148
- * `comprovado por` are different claims. The corpus already contains
149
- * "aprovado por lucas@…" inside a code sample, so this is a live shape,
150
- * not a hypothetical.
151
- *
152
- * @type {RegExp}
153
- */
34
+ // Three real typesettings: backtick before the clause, after it, or the clause in
35
+ // bold mid-sentence. The non-letter prefix keeps `aprovado por` out.
154
36
  export const PROVED_BY_TRIGGER_RE = /(?:^|[^a-zA-Z])provado por/gi;
155
37
 
156
- /**
157
- * One path token after the clause, plus the separator that may precede it:
158
- * whitespace, backtick, `,`, `;`, the word `e`, and the markdown emphasis
159
- * characters `*` and `_`. The emphasis characters are in the class because of
160
- * typesetting C — without them, `**provado por**` armed the trigger and then
161
- * matched no path at all, losing both of its files in silence.
162
- *
163
- * Anchored: it only ever reads from the cursor, so prose ends the scan instead
164
- * of being searched through.
165
- *
166
- * @type {RegExp}
167
- */
38
+ // A path needs a `/` and a closing backtick, so denials ("sem cláusula `provado por`")
39
+ // derive nothing. `*`/`_` are separators for the bold form. Anchored: prose ends the scan.
168
40
  export const PROVED_BY_PATH_RE =
169
41
  /^[\s`,;*_]*(?:\be\b[\s`,;*_]*)?([A-Za-z0-9_.][A-Za-z0-9_.\-]*(?:\/[A-Za-z0-9_.\-]*)+)`/;
170
42
 
171
43
  /**
172
- * Derive the list of files a standard claims to be *proved by*.
173
- *
174
- * Reads the SAME header {@link parseVerifiedAgainst} reads — deliberately, and
175
- * from this module, because a second regex over the same sentence in
176
- * `scripts/generate-standards-registry.js` is precisely the drift the module
177
- * docblock opens with.
178
- *
179
- * @param {string} verifiedAgainst - The subject captured by
180
- * {@link parseVerifiedAgainst} (group 1), not the whole markdown file.
181
- * @returns {string[]} Paths in declaration order, de-duplicated. `[]` means
182
- * "the header was read and declares no proof" — a documental verification —
183
- * never "not asked". The caller distinguishes the two by whether the header
184
- * parsed at all.
44
+ * Takes the subject from parseVerifiedAgainst, not the whole file. `[]` means the
45
+ * header declares no proof, never "not asked".
185
46
  */
186
47
  export function deriveProvedBy(verifiedAgainst) {
187
48
  if (typeof verifiedAgainst !== 'string' || verifiedAgainst.length === 0) return [];
@@ -199,16 +60,7 @@ export function deriveProvedBy(verifiedAgainst) {
199
60
  return paths;
200
61
  }
201
62
 
202
- /**
203
- * Count unresolved `// VERIFY` markers that sit INSIDE a fenced code block.
204
- *
205
- * The fence rule is what separates a real marker from prose that merely quotes
206
- * one (`marcados \`// VERIFY:\` no código`). It lives here, next to the regex,
207
- * so every consumer counts the same thing.
208
- *
209
- * @param {string} text - Full markdown text of the standard.
210
- * @returns {number}
211
- */
63
+ /** Only markers inside a fenced code block count: prose quoting one is not a marker. */
212
64
  export function countVerifyMarkers(text) {
213
65
  if (typeof text !== 'string' || text.length === 0) return 0;
214
66
  let inFence = false;
@@ -220,20 +72,7 @@ export function countVerifyMarkers(text) {
220
72
  return count;
221
73
  }
222
74
 
223
- /**
224
- * Read and parse `framework/ai-pin.json`.
225
- *
226
- * Throws — never falls back to a default — when the pin is missing or invalid:
227
- * without a pin there is no ruler, and pretending there is one is the original
228
- * defect this feature removes. The message names the file so the failure is
229
- * diagnosable from a CI log alone.
230
- *
231
- * @param {string} [root=DEFAULT_ROOT] - Repository root that contains `framework/`.
232
- * @returns {{ pinnedAt: string, source: string, targetFramework: string, sdk: string,
233
- * packages: Record<string, {version: string, published: string, role: string}>,
234
- * toolchain: Record<string, {version: string}> }}
235
- * @throws {Error} When the file is absent or is not valid JSON.
236
- */
75
+ /** Throws, never defaults, when the pin is missing or invalid: without a pin there is no ruler. */
237
76
  export function readAiPin(root = DEFAULT_ROOT) {
238
77
  const pinPath = join(root, AI_PIN_RELATIVE);
239
78
  if (!existsSync(pinPath)) {
@@ -246,7 +85,6 @@ export function readAiPin(root = DEFAULT_ROOT) {
246
85
  }
247
86
  }
248
87
 
249
- /** Parse an ISO calendar date into a UTC epoch, timezone-independent. */
250
88
  function isoToUtc(value, label) {
251
89
  const m = typeof value === 'string' ? ISO_DATE_RE.exec(value) : null;
252
90
  if (!m) {
@@ -256,18 +94,8 @@ function isoToUtc(value, label) {
256
94
  }
257
95
 
258
96
  /**
259
- * Age of a standard's verification, in whole days, measured against the pin.
260
- *
261
- * Both dates are read as UTC calendar days, so the result does not depend on
262
- * the runner's timezone. The value is NEGATIVE when the standard is newer than
263
- * the pin — the caller must treat that as a PIN error (a `pinnedAt` in the
264
- * future), never as a fresh standard: otherwise every standard could be
265
- * "renewed" by editing one date.
266
- *
267
- * @param {string} lastVerified - ISO date from the standard's header.
268
- * @param {string} pinnedAt - ISO date from `framework/ai-pin.json`.
269
- * @returns {number} Whole days between the two, possibly negative.
270
- * @throws {TypeError} When either argument is not an ISO date.
97
+ * Negative when the standard is newer than the pin: the caller must treat that as a
98
+ * pin error, never as fresh, or any standard could be "renewed" by editing one date.
271
99
  */
272
100
  export function ageInDaysAgainstPin(lastVerified, pinnedAt) {
273
101
  const verified = isoToUtc(lastVerified, 'lastVerified');
@@ -1,36 +1,3 @@
1
- /**
2
- * Eval Node — the `runner: "evals"` kind of `project.tests[]`.
3
- *
4
- * WHY THIS IS A RUNNER KIND AND NOT A SIXTH VERIFY NODE
5
- *
6
- * An eval IS `dotnet test` with a category filter: one command, one cwd, one
7
- * clock, one exit code. Modelling it as something other than a test suite would
8
- * mean a second copy of the executor, a second decision for `--filter`, a second
9
- * decision for `--linux`, a second place for `expectedCount` — and a new line in
10
- * a residency-capped charter that has zero slack. As a runner kind it inherits
11
- * all of that for free, and the ONE thing the rollup cannot express (eval red vs
12
- * unit-test red) is recovered by the verify stamp, which records `nodes.evals`
13
- * separately.
14
- *
15
- * WHAT THIS MODULE OWNS
16
- *
17
- * - the STATES of a cache tree, judged BEFORE `dotnet test` is invoked. Paying
18
- * for a whole suite to discover at the end that the cache did not serve is
19
- * the worst possible ordering;
20
- * - the `MORPH_EVAL_*` variables the CLI DICTATES, so the same xunit suite is
21
- * runnable by `verify` without a second configuration path;
22
- * - reading the summary the fixture writes — where **null is UNKNOWN, never
23
- * zero**.
24
- *
25
- * WHAT IT DELIBERATELY DOES NOT OWN
26
- *
27
- * The pin. `readAiPin()` (src/lib/standards/verified-against.js) is the single
28
- * reader of `framework/ai-pin.json`; a second parser here is how two truths
29
- * about "which version is pinned" begin.
30
- *
31
- * @module eval-node
32
- */
33
-
34
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
35
2
  import { join, resolve, sep } from 'path';
36
3
  import { tmpdir } from 'os';
@@ -38,28 +5,19 @@ import { tmpdir } from 'os';
38
5
  import { isInside } from './test-runner.js';
39
6
  import { resolveAiPin } from '../ai/pin-drift.js';
40
7
 
41
- /** The runner kind. One spelling, imported by every reader. */
42
8
  export const EVAL_RUNNER = 'evals';
43
9
 
44
- /** Stamp file at the root of a cache tree — the SAME name the .NET fixture writes. */
10
+ /** The SAME name the .NET fixture writes. */
45
11
  export const EVAL_STAMP_FILE = '.morph-eval-cache.json';
46
12
 
47
- /** Default dataset root, relative to the cache root's parent. */
13
+ /** Relative to the cache root's parent. */
48
14
  export const DEFAULT_DATASET_DIR = 'eval-datasets';
49
15
 
50
- /** Default credential file. Gitignored, like `e2e.auth.envFile`; never in the committed config. */
51
16
  export const DEFAULT_ENV_FILE = '.env.evals';
52
17
 
53
18
  /**
54
- * The pin entries that decide the FORMAT of a response cache.
55
- *
56
- * Only the `Microsoft.Extensions.AI` family: the cache key is computed by that
57
- * library, and its own docs say the hash is not guaranteed stable across
58
- * releases. Bumping an unrelated provider package (OpenAI, Google.GenAI) cannot
59
- * invalidate a cache tree, and a stamp that reacted to it would fail for
60
- * nothing — a guardrail that cries wolf gets switched off.
61
- *
62
- * @param {Record<string, {version: string}>} packages
19
+ * Only the `Microsoft.Extensions.AI` family decides the cache format: that library
20
+ * computes the cache key. Reacting to unrelated provider bumps would fail for nothing.
63
21
  * @returns {Record<string, string>} name → exact version
64
22
  */
65
23
  export function cacheRelevantPackages(packages) {
@@ -72,13 +30,6 @@ export function cacheRelevantPackages(packages) {
72
30
  return out;
73
31
  }
74
32
 
75
- /**
76
- * Resolve every path an eval node names, against the project root.
77
- *
78
- * @param {import('./test-nodes.js').TestNode & {evals?: object}} node
79
- * @param {string} projectPath
80
- * @returns {{cacheRoot: string, datasetRoot: string, resultsRoot: string, envFile: string}}
81
- */
82
33
  export function resolveEvalPaths(node, projectPath) {
83
34
  const root = resolve(projectPath);
84
35
  const evals = node?.evals ?? {};
@@ -88,25 +39,14 @@ export function resolveEvalPaths(node, projectPath) {
88
39
  return {
89
40
  cacheRoot,
90
41
  datasetRoot: evals.datasetPath ? resolve(root, evals.datasetPath) : join(beside, DEFAULT_DATASET_DIR),
91
- // Results live OUTSIDE the committed cache tree by construction — they carry
92
- // stopwatch latency, token usage and the cache-hit flag, so committing them
93
- // is a guaranteed diff on every run with no signal in it.
42
+ // Outside the committed cache tree: results carry latency and token usage,
43
+ // so committing them is a diff on every run with no signal in it.
94
44
  resultsRoot: join(beside, 'eval-results'),
95
45
  envFile: resolve(root, evals.envFile ?? DEFAULT_ENV_FILE),
96
46
  };
97
47
  }
98
48
 
99
- /**
100
- * First problem with a node's `evals` block, or null when it is well formed.
101
- *
102
- * Fail-CLOSED, like the rest of `validateTestNodes`: a typo here would make the
103
- * eval silently disappear, which is the exact defect this feature exists to
104
- * prevent.
105
- *
106
- * @param {*} raw - the raw `tests[]` entry
107
- * @param {string|null} projectPath
108
- * @returns {string|null}
109
- */
49
+ /** Fail-CLOSED: a typo must never make the eval silently disappear. */
110
50
  export function evalBlockProblem(raw, projectPath) {
111
51
  const evals = raw?.evals;
112
52
 
@@ -129,7 +69,6 @@ export function evalBlockProblem(raw, projectPath) {
129
69
  for (const key of ['cachePath', 'datasetPath']) {
130
70
  const value = evals[key];
131
71
  if (typeof value !== 'string' || !value.trim()) continue;
132
- // Same rule as `cwd`: a path that leaves the root addresses ANOTHER tree.
133
72
  if (!isInside(resolve(root, value.trim()), root)) {
134
73
  return `evals.${key} "${value}" resolves outside the project root`;
135
74
  }
@@ -138,7 +77,6 @@ export function evalBlockProblem(raw, projectPath) {
138
77
  return null;
139
78
  }
140
79
 
141
- /** Does this cache tree hold at least one entry, beyond the stamp? */
142
80
  function cacheHasEntries(cacheRoot) {
143
81
  const walk = (dir) => {
144
82
  let entries;
@@ -160,7 +98,7 @@ function cacheHasEntries(cacheRoot) {
160
98
  return walk(cacheRoot);
161
99
  }
162
100
 
163
- /** Read the stamp the .NET fixture wrote. null for absent OR unreadable — Check tells them apart. */
101
+ /** null for absent OR unreadable. */
164
102
  export function readEvalStamp(cacheRoot) {
165
103
  const path = join(cacheRoot, EVAL_STAMP_FILE);
166
104
  if (!existsSync(path)) return null;
@@ -173,21 +111,8 @@ export function readEvalStamp(cacheRoot) {
173
111
  }
174
112
 
175
113
  /**
176
- * Decide, BEFORE spending a single second of suite clock, whether this eval node
177
- * can run.
178
- *
179
- * The order is the order of the spec, and it is not arbitrary: the three cache
180
- * states are judged before the command is invoked, because discovering at the
181
- * END of a suite that the cache did not serve costs the whole clock and tells
182
- * the reader nothing they could not have been told first.
183
- *
184
- * **There is no `skip` for a bad cache.** A skip here would recreate the hole
185
- * this harness closed twice: a green gate over a verification that never
186
- * happened.
187
- *
188
- * @param {string} projectPath
189
- * @param {object} node - a resolved test node with `runner === 'evals'`
190
- * @param {{refresh?: boolean}} [options]
114
+ * Judged BEFORE `dotnet test`: finding a bad cache at the end costs the whole clock.
115
+ * A bad cache is `fail`, never `skip` — a skip is a green gate over nothing.
191
116
  * @returns {{status: 'fail'|null, reason: string|null}} status null means RUN
192
117
  */
193
118
  export function evalPreflight(projectPath, node, options = {}) {
@@ -197,8 +122,7 @@ export function evalPreflight(projectPath, node, options = {}) {
197
122
 
198
123
  const { cacheRoot, envFile } = resolveEvalPaths(node, projectPath);
199
124
 
200
- // In refresh the tree is being (re)written; demanding a populated, stamped
201
- // cache would make the FIRST cache impossible to create.
125
+ // Demanding a populated, stamped cache in refresh would make the FIRST cache impossible.
202
126
  if (refresh) {
203
127
  if (!existsSync(envFile)) {
204
128
  return {
@@ -214,15 +138,12 @@ export function evalPreflight(projectPath, node, options = {}) {
214
138
  }
215
139
 
216
140
  if (!cacheHasEntries(cacheRoot)) {
217
- // Distinct from `missing` on purpose: an empty COMMITTED directory is far
218
- // more likely a `.gitignore` mistake than a deliberate absence, and the two
219
- // ask for different fixes.
141
+ // Distinct from `missing`: an empty committed directory is usually a `.gitignore` mistake.
220
142
  return { status: 'fail', reason: `eval-cache-empty: ${cacheRoot} não contém nenhuma entrada de cache` };
221
143
  }
222
144
 
223
145
  const pin = resolveAiPin(projectPath);
224
146
  if (!pin) {
225
- // No pin in the project or in the CLI package: nothing to stamp against.
226
147
  return { status: null, reason: null };
227
148
  }
228
149
 
@@ -252,15 +173,7 @@ export function evalPreflight(projectPath, node, options = {}) {
252
173
  }
253
174
 
254
175
  /**
255
- * The `MORPH_EVAL_*` variables the CLI dictates for one eval node.
256
- *
257
- * The summary path is chosen HERE, by the CLI, and not by the fixture: two eval
258
- * nodes in the same `verify` must not collide, and the process that reads the
259
- * file is the one that should decide where it lives.
260
- *
261
- * @param {object} node
262
- * @param {string} projectPath
263
- * @param {{refresh?: boolean}} [options]
176
+ * The CLI picks the summary path, not the fixture: two eval nodes in one verify must not collide.
264
177
  * @returns {{env: Record<string,string>, summaryPath: string}}
265
178
  */
266
179
  export function evalNodeEnv(node, projectPath, options = {}) {
@@ -279,31 +192,19 @@ export function evalNodeEnv(node, projectPath, options = {}) {
279
192
 
280
193
  const pin = resolveAiPin(projectPath);
281
194
  if (pin) {
282
- // Compact, and only the packages that decide the cache format. The fixture
283
- // stamps from THIS, so it never has to read (or parse) the pin itself.
284
195
  env.MORPH_EVAL_PIN = JSON.stringify({
285
196
  pinnedAt: pin.pinnedAt,
286
197
  packages: cacheRelevantPackages(pin.packages),
287
198
  });
288
199
  }
289
200
 
290
- // The credential file goes on top, same pattern as `e2e.auth.envFile`. Absent
291
- // is NOT an error offline: that is the whole point of a warm cache.
201
+ // Absent credential file is NOT an error offline: that is the point of a warm cache.
292
202
  Object.assign(env, readEnvFile(envFile));
293
203
 
294
204
  return { env, summaryPath };
295
205
  }
296
206
 
297
- /**
298
- * Parse a `KEY=value` file. Blank lines and `#` comments ignored; surrounding
299
- * quotes stripped; the first `=` splits.
300
- *
301
- * Fail-open to `{}`: an unreadable credential file must not take down a run that
302
- * did not need a credential in the first place.
303
- *
304
- * @param {string} path
305
- * @returns {Record<string,string>}
306
- */
207
+ /** Fail-open to `{}`: an unreadable credential file must not take down a run that needed none. */
307
208
  export function readEnvFile(path) {
308
209
  if (!path || !existsSync(path)) return {};
309
210
  let text;
@@ -329,14 +230,7 @@ export function readEnvFile(path) {
329
230
  return out;
330
231
  }
331
232
 
332
- /**
333
- * Read the summary the fixture wrote.
334
- *
335
- * @param {string} path
336
- * @returns {object|null} **null is UNKNOWN, never zero.** A summary nobody could
337
- * read is not a suite that evaluated zero scenarios: collapsing the two would
338
- * manufacture a count drop against `expectedCount` out of a missing file.
339
- */
233
+ /** @returns {object|null} null is UNKNOWN, never zero: a missing file must not fabricate a count drop. */
340
234
  export function readEvalSummary(path) {
341
235
  if (!path || !existsSync(path)) return null;
342
236
  try {
@@ -349,37 +243,15 @@ export function readEvalSummary(path) {
349
243
  }
350
244
 
351
245
  /**
352
- * How many scenarios an eval run evaluated.
353
- *
354
- * The ONLY source of an eval node's count. The `dotnet` extractor reads the same
355
- * run but counts xunit cases — another unit (#110) — so an unreadable summary
356
- * yields null: UNKNOWN, never 0, and never the extractor's number.
357
- *
358
- * @param {object|null} summary
359
- * @returns {number|null}
246
+ * The ONLY source of an eval node's count. The `dotnet` extractor counts xunit cases,
247
+ * another unit, so an unreadable summary is null — never 0, never the extractor's number.
360
248
  */
361
249
  export function evalScenarioCount(summary) {
362
250
  const n = summary?.scenarios;
363
251
  return Number.isInteger(n) ? n : null;
364
252
  }
365
253
 
366
- /**
367
- * The NAMED failures of an eval run, one readable line each.
368
- *
369
- * The summary publishes `failures[]` (scenario, assertion, kind, reason) and
370
- * without this reader the field would be written and never surfaced — a
371
- * published field with no consumer, the same shape #91 closed on `superseded`.
372
- * Worse here than usual: the whole point of `failures[]` is that a red eval
373
- * names what broke, and a name nobody prints is not a name.
374
- *
375
- * Defensive by construction, because this parses a file produced by ANOTHER
376
- * process: a summary that is absent, unreadable, or carries a malformed entry
377
- * yields fewer lines, never a throw. A crash here would turn "the eval failed"
378
- * into "the verify crashed", which reports the wrong defect.
379
- *
380
- * @param {object|null} summary
381
- * @returns {string[]} readable lines; empty when there is nothing to name
382
- */
254
+ /** Never throws on a malformed summary: a crash would report "verify crashed" instead of "eval failed". */
383
255
  export function evalFailureLines(summary) {
384
256
  const failures = summary?.failures;
385
257
  if (!Array.isArray(failures)) return [];
@@ -395,16 +267,7 @@ export function evalFailureLines(summary) {
395
267
  });
396
268
  }
397
269
 
398
- /**
399
- * Why an otherwise-green eval run must still be red, or null when it is fine.
400
- *
401
- * One case, and it is the guardrail-inert shape this repository has already paid
402
- * to learn: a suite that evaluated ZERO scenarios and exited 0. The exit code
403
- * alone cannot see it, because nothing failed — nothing ran.
404
- *
405
- * @param {object|null} summary
406
- * @returns {string|null}
407
- */
270
+ /** Zero scenarios with exit 0 is red: the exit code cannot see a suite where nothing ran. */
408
271
  export function evalEmptyRunProblem(summary) {
409
272
  if (summary && summary.scenarios === 0) {
410
273
  return 'eval-empty-run: a suíte de eval avaliou 0 cenários e saiu verde';
@@ -412,16 +275,7 @@ export function evalEmptyRunProblem(summary) {
412
275
  return null;
413
276
  }
414
277
 
415
- /**
416
- * Warning for a run whose cache went cold — the tree changed and must be committed.
417
- *
418
- * A warning and not a failure, deliberately: spending money nobody asked for is
419
- * bad, but lying about the result is worse. Without the warning, a freshly
420
- * written cache stays on the disk of whoever ran it and CI misses tomorrow.
421
- *
422
- * @param {object|null} summary
423
- * @returns {string|null}
424
- */
278
+ /** A warning, not a failure: without it the fresh cache stays on one disk and CI misses tomorrow. */
425
279
  export function evalColdCacheWarning(summary) {
426
280
  if (summary?.cache === 'cold') {
427
281
  const misses = Number.isInteger(summary.misses) ? summary.misses : '?';
@@ -430,14 +284,12 @@ export function evalColdCacheWarning(summary) {
430
284
  return null;
431
285
  }
432
286
 
433
- /** Relative form of a path, for messages that should not shout an absolute path. */
434
287
  export function relativeToProject(projectPath, target) {
435
288
  const root = resolve(projectPath) + sep;
436
289
  const full = resolve(target);
437
290
  return full.startsWith(root) ? full.slice(root.length).replace(/\\/g, '/') : full;
438
291
  }
439
292
 
440
- /** Is `path` a directory? Fail-open to false. */
441
293
  export function isDirectory(path) {
442
294
  try {
443
295
  return statSync(path).isDirectory();