mandrel 2.31.0 → 2.33.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 (267) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +12 -9
  4. package/.agents/docs/agentrc-reference.json +63 -58
  5. package/.agents/docs/configuration.md +275 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/git-conventions-reference.md +27 -27
  10. package/.agents/rules/git-conventions.md +4 -2
  11. package/.agents/rules/known-tooling-behavior.md +66 -30
  12. package/.agents/rules/testing-standards.md +35 -71
  13. package/.agents/runtime-deps.json +2 -2
  14. package/.agents/schemas/agentrc.schema.json +1955 -1410
  15. package/.agents/schemas/lifecycle/README.md +21 -14
  16. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  17. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  18. package/.agents/scripts/README.md +16 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/audit-to-stories.js +160 -41
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +159 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +108 -124
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +81 -35
  38. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
  39. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  40. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  41. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  42. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  43. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  44. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  45. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  46. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  47. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  48. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  49. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  50. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  51. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  52. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  53. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  54. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  55. package/.agents/scripts/lib/child-exec.js +193 -0
  56. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  57. package/.agents/scripts/lib/cli-args.js +64 -0
  58. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  59. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  60. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  61. package/.agents/scripts/lib/config/commands.js +3 -5
  62. package/.agents/scripts/lib/config/explain.js +5 -7
  63. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  64. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  65. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  66. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  67. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  68. package/.agents/scripts/lib/config/gates/index.js +5 -2
  69. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  70. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  71. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  72. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  73. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  74. package/.agents/scripts/lib/config/github.js +0 -1
  75. package/.agents/scripts/lib/config/limits.js +3 -48
  76. package/.agents/scripts/lib/config/qa.js +105 -0
  77. package/.agents/scripts/lib/config/runners.js +38 -16
  78. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  79. package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -56
  80. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  81. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  82. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  83. package/.agents/scripts/lib/crap-utils.js +84 -520
  84. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  85. package/.agents/scripts/lib/degraded-mode.js +2 -2
  86. package/.agents/scripts/lib/dependency-parser.js +20 -7
  87. package/.agents/scripts/lib/doc-tiers.js +3 -3
  88. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  89. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  90. package/.agents/scripts/lib/findings/provenance-field.js +135 -0
  91. package/.agents/scripts/lib/findings/route-finding.js +57 -8
  92. package/.agents/scripts/lib/fs-walk.js +52 -0
  93. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  94. package/.agents/scripts/lib/git-utils.js +16 -36
  95. package/.agents/scripts/lib/knip-config-resolver.js +181 -0
  96. package/.agents/scripts/lib/knip-entry-sync.js +508 -0
  97. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  98. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  99. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  100. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  101. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  102. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  103. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  104. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  105. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  106. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  107. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  108. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  109. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  110. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  111. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  112. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +7 -26
  114. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
  115. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
  116. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
  117. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  118. package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
  119. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  120. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  131. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  132. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  133. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  134. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  135. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  136. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  137. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  138. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +116 -1
  139. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  140. package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
  141. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  142. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  143. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  144. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  145. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  146. package/.agents/scripts/lib/signals/index.js +8 -6
  147. package/.agents/scripts/lib/signals/schema.js +20 -25
  148. package/.agents/scripts/lib/signals/write.js +8 -8
  149. package/.agents/scripts/lib/story-body/footer-block.js +97 -0
  150. package/.agents/scripts/lib/story-body/story-body.js +18 -81
  151. package/.agents/scripts/lib/temp-retention.js +1 -1
  152. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  153. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  154. package/.agents/scripts/lib/wave-runner/footprint.js +306 -0
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
  156. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  157. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  158. package/.agents/scripts/lib/worktree-manager.js +2 -3
  159. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  160. package/.agents/scripts/pr-watch-with-update.js +7 -5
  161. package/.agents/scripts/providers/github/blocked-by-add.js +25 -10
  162. package/.agents/scripts/providers/github/cache.js +2 -2
  163. package/.agents/scripts/providers/github/comments.js +6 -28
  164. package/.agents/scripts/providers/github/compose.js +0 -15
  165. package/.agents/scripts/providers/github/errors.js +10 -27
  166. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  167. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  168. package/.agents/scripts/providers/github.js +4 -7
  169. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  170. package/.agents/scripts/resolve-stories.js +21 -7
  171. package/.agents/scripts/run-lint.js +61 -61
  172. package/.agents/scripts/run-test-profile.js +6 -6
  173. package/.agents/scripts/run-verify.js +48 -30
  174. package/.agents/scripts/single-story-close.js +20 -0
  175. package/.agents/scripts/single-story-init.js +12 -35
  176. package/.agents/scripts/stories-wave-tick.js +192 -9
  177. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  178. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  179. package/.agents/skills/skills.index.json +1 -11
  180. package/.agents/workflows/audit-documentation.md +5 -6
  181. package/.agents/workflows/audit-to-stories.md +28 -2
  182. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  183. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  184. package/.agents/workflows/helpers/code-review.md +3 -8
  185. package/.agents/workflows/helpers/deliver-reference.md +30 -2
  186. package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
  187. package/.agents/workflows/helpers/plan-reference.md +76 -0
  188. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  189. package/.agents/workflows/mandrel-update.md +10 -10
  190. package/.agents/workflows/qa-assist.md +15 -20
  191. package/.agents/workflows/qa-explore.md +9 -8
  192. package/README.md +1 -1
  193. package/docs/CHANGELOG.md +58 -0
  194. package/lib/migrations/index.js +2 -0
  195. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  196. package/package.json +14 -5
  197. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  198. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  199. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  200. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  201. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  202. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  203. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  204. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  205. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  206. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  207. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  208. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  209. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  210. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  211. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  212. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  213. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  214. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  215. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  216. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  217. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  218. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  219. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  220. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  221. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  222. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  223. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  224. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  225. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  226. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  227. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  228. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  229. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  230. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  231. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  232. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  233. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  234. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  235. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  236. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  237. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  238. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  239. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  240. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  241. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  242. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  243. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  244. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  245. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  246. package/.agents/scripts/lib/planning-corpus.js +0 -37
  247. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  248. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  249. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  250. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  251. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  252. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  253. package/.agents/scripts/lib/signals/read.js +0 -268
  254. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  255. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  256. package/.agents/scripts/lib/story-plan.js +0 -379
  257. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  258. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  259. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  260. package/.agents/scripts/lint-baseline.js +0 -507
  261. package/.agents/scripts/providers/github/prs.js +0 -103
  262. package/.agents/scripts/signals-view.js +0 -309
  263. package/.agents/scripts/story-plan.js +0 -370
  264. package/.agents/scripts/sync-branch-from-base.js +0 -149
  265. package/.agents/scripts/validate-docs-freshness.js +0 -314
  266. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  267. package/.agents/workflows/helpers/signals.md +0 -112
@@ -0,0 +1,314 @@
1
+ // .agents/scripts/lib/baselines/scope-inventory.js
2
+ //
3
+ // Story #5012 — the ONE in-scope file inventory the honesty surface reads.
4
+ //
5
+ // A ratchet gate can be green while measuring almost nothing: nothing checks
6
+ // that a committed baseline's row set still describes the tree it claims to
7
+ // describe. Both instruments that now do — `check-baseline-scope.js` (assert)
8
+ // and `prune-baseline-orphans.js` (remedy) — resolve "which files is this kind
9
+ // supposed to have rows for?" through this module, so the gate and the pruner
10
+ // cannot disagree about scope and start fighting each other.
11
+ //
12
+ // ## Recompute from the gate's own config, never from a second walker
13
+ //
14
+ // The inventory is derived from **the same configuration the kind's refresh
15
+ // scorer reads**, through the same helpers:
16
+ //
17
+ // - `coverage` — `.c8rc.cjs` `include` / `exclude`, applied with
18
+ // `coverage-baseline.js#buildScopePredicate`, exactly as
19
+ // `refresh-service.js#buildDefaultCoverageScorer` does.
20
+ // - every other file-keyed kind — `delivery.quality.gates.<kind>.targetDirs`
21
+ // / `.ignoreGlobs`, walked with `maintainability-utils.js#scanDirectory`,
22
+ // exactly as `refresh-service.js#buildDefaultMaintainabilityScorer` does.
23
+ //
24
+ // A second walker written here would report the two implementations
25
+ // disagreeing as divergence — the same failure mode
26
+ // `check-baseline-drift.js` avoids by re-scoring through
27
+ // `resolveDefaultScorer` rather than a private scorer.
28
+ //
29
+ // ## Measurement-free by contract
30
+ //
31
+ // Nothing in this module runs a scorer, reads `coverage/coverage-final.json`,
32
+ // or needs a test run to have happened. It walks directories and matches
33
+ // globs. That is what makes the gate runnable on any checkout, which is in
34
+ // turn what makes hard-failing on a stale row defensible.
35
+
36
+ import { createRequire } from 'node:module';
37
+ import path from 'node:path';
38
+
39
+ import { buildScopePredicate } from '../coverage-baseline.js';
40
+ import { scanDirectory } from '../maintainability-utils.js';
41
+
42
+ /**
43
+ * Per-kind classification of which divergence directions may be asserted.
44
+ *
45
+ * **Only a kind whose producer emits one row per in-scope file may assert the
46
+ * MISSING direction.** This is not a conservatism knob — it is arithmetic.
47
+ * Measured against this repository on 2026-08-05, asserting MISSING densely
48
+ * yields 128 phantom rows for `crap` (rows are per-method and coverage-gated,
49
+ * so a file with no scorable method legitimately has none) and 510 for
50
+ * `duplication` (a row exists only where a clone was found). A gate that
51
+ * reports 638 phantom findings on a healthy tree is a gate nobody reads.
52
+ *
53
+ * `lint` and `mutation` are sparse for the same reason — a clean file has no
54
+ * lint row, an untested file has no mutation row. `lighthouse` (keyed on
55
+ * `route`) and `bundle-size` (keyed on `bundle`) are not file-keyed at all, so
56
+ * neither direction is meaningful: their keys name artefacts, not paths on
57
+ * disk, and a directory walk can say nothing about them.
58
+ *
59
+ * @type {Readonly<Record<string, { keyField: string, directions: readonly string[] }>>}
60
+ */
61
+ export const KIND_SCOPE_POLICY = Object.freeze({
62
+ coverage: Object.freeze({
63
+ keyField: 'path',
64
+ directions: Object.freeze(['missing', 'extra']),
65
+ }),
66
+ maintainability: Object.freeze({
67
+ keyField: 'path',
68
+ directions: Object.freeze(['missing', 'extra']),
69
+ }),
70
+ crap: Object.freeze({
71
+ keyField: 'path',
72
+ directions: Object.freeze(['extra']),
73
+ }),
74
+ duplication: Object.freeze({
75
+ keyField: 'path',
76
+ directions: Object.freeze(['extra']),
77
+ }),
78
+ lint: Object.freeze({
79
+ keyField: 'path',
80
+ directions: Object.freeze(['extra']),
81
+ }),
82
+ mutation: Object.freeze({
83
+ keyField: 'path',
84
+ directions: Object.freeze(['extra']),
85
+ }),
86
+ lighthouse: Object.freeze({
87
+ keyField: 'route',
88
+ directions: Object.freeze([]),
89
+ }),
90
+ 'bundle-size': Object.freeze({
91
+ keyField: 'bundle',
92
+ directions: Object.freeze([]),
93
+ }),
94
+ });
95
+
96
+ /** Every kind the honesty surface knows about, in report order. */
97
+ export const SCOPE_KINDS = Object.freeze(Object.keys(KIND_SCOPE_POLICY));
98
+
99
+ /**
100
+ * The directions `kind` may assert. Unknown kinds assert nothing — a kind this
101
+ * module has never heard of has no scope model, and inventing one would be the
102
+ * phantom-row failure above with extra steps.
103
+ *
104
+ * @param {string} kind
105
+ * @returns {readonly string[]}
106
+ */
107
+ export function directionsFor(kind) {
108
+ return KIND_SCOPE_POLICY[kind]?.directions ?? [];
109
+ }
110
+
111
+ /**
112
+ * The row field `kind` keys on — `path` for the file-keyed kinds, `route` for
113
+ * lighthouse, `bundle` for bundle-size. Mirrors `kinds/<kind>.js#keyField`;
114
+ * kept here so a caller can classify without importing the kernel.
115
+ *
116
+ * @param {string} kind
117
+ * @returns {string}
118
+ */
119
+ export function keyFieldFor(kind) {
120
+ return KIND_SCOPE_POLICY[kind]?.keyField ?? 'path';
121
+ }
122
+
123
+ /**
124
+ * True when `kind` keys its rows on a repo-relative file path, so a directory
125
+ * walk can be compared against its row set at all.
126
+ *
127
+ * @param {string} kind
128
+ * @returns {boolean}
129
+ */
130
+ export function isFileKeyed(kind) {
131
+ return keyFieldFor(kind) === 'path' && Object.hasOwn(KIND_SCOPE_POLICY, kind);
132
+ }
133
+
134
+ /** Glob metacharacters that end a pattern's literal prefix. */
135
+ const GLOB_META = /[*?[\]{}!+@()]/;
136
+
137
+ /**
138
+ * Derive directory roots to walk from a list of include globs, by taking each
139
+ * pattern's literal leading path segments.
140
+ *
141
+ * The coverage gate's schema is closed and declares no `targetDirs` — its
142
+ * scope lives entirely in `.c8rc.cjs`. Without this derivation the coverage
143
+ * walk has no root to start from, so there is nothing to compare the row set
144
+ * against and the whole MISSING direction goes silently inert for the one kind
145
+ * whose producer is densest.
146
+ *
147
+ * `'.agents/scripts/**'` yields `.agents/scripts`; a pattern that is glob from
148
+ * its first segment (`'**​/*.js'`) yields `.`, the repo root.
149
+ *
150
+ * @param {string[]} globs
151
+ * @returns {string[]} De-duplicated roots, repo-relative POSIX.
152
+ */
153
+ export function deriveWalkRoots(globs) {
154
+ const roots = new Set();
155
+ for (const glob of Array.isArray(globs) ? globs : []) {
156
+ if (typeof glob !== 'string' || glob.length === 0) continue;
157
+ const literal = [];
158
+ for (const segment of glob.split('/')) {
159
+ if (GLOB_META.test(segment)) break;
160
+ literal.push(segment);
161
+ }
162
+ roots.add(literal.join('/') || '.');
163
+ }
164
+ return [...roots];
165
+ }
166
+
167
+ /**
168
+ * Walk `roots` under `cwd` and return canonical repo-relative POSIX paths.
169
+ *
170
+ * Delegates to `scanDirectory`, which already owns the supported-extension set
171
+ * and the ignored-directory set (`node_modules`, `.git`, `temp`, `.worktrees`,
172
+ * …) that every scorer walks with. Passing `ignoreGlobs` straight through
173
+ * means an ignored file is dropped by the identical matcher the scorer uses.
174
+ *
175
+ * @param {{ cwd: string, roots: string[], ignoreGlobs?: string[] }} params
176
+ * @returns {string[]}
177
+ */
178
+ function walkRoots({ cwd, roots, ignoreGlobs = [] }) {
179
+ const absolute = [];
180
+ for (const root of roots) {
181
+ const abs = path.isAbsolute(root) ? root : path.resolve(cwd, root);
182
+ scanDirectory(abs, absolute, { cwd, ignoreGlobs });
183
+ }
184
+ return absolute.map((abs) =>
185
+ path.relative(cwd, abs).split(path.sep).join('/'),
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Build the coverage inventory from `.c8rc.cjs`.
191
+ *
192
+ * Roots come from the literal prefixes of `include`; the walked set is then
193
+ * filtered through the same `buildScopePredicate` the coverage scorer applies,
194
+ * so `exclude` is honoured by one implementation rather than two.
195
+ *
196
+ * @param {{ cwd: string, requireFn?: (id: string) => object }} params
197
+ * @returns {{ files: string[] | null, roots: string[], degraded: boolean, reason: string | null }}
198
+ */
199
+ function coverageInventory({ cwd, requireFn }) {
200
+ const load = requireFn ?? createRequire(path.join(cwd, 'noop.cjs'));
201
+ let config;
202
+ try {
203
+ config = load(path.resolve(cwd, '.c8rc.cjs'));
204
+ } catch (err) {
205
+ return {
206
+ files: null,
207
+ roots: [],
208
+ degraded: true,
209
+ reason: `.c8rc.cjs unreadable: ${err?.message ?? err}`,
210
+ };
211
+ }
212
+ const include = Array.isArray(config?.include) ? config.include : [];
213
+ const exclude = Array.isArray(config?.exclude) ? config.exclude : [];
214
+ if (include.length === 0) {
215
+ return {
216
+ files: null,
217
+ roots: [],
218
+ degraded: true,
219
+ reason: '.c8rc.cjs declares no include globs',
220
+ };
221
+ }
222
+ const roots = deriveWalkRoots(include);
223
+ const inScope = buildScopePredicate({ include, exclude });
224
+ return {
225
+ files: walkRoots({ cwd, roots }).filter(inScope),
226
+ roots,
227
+ degraded: false,
228
+ reason: null,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Build the inventory for a `targetDirs`-configured kind (everything except
234
+ * coverage). Reads the gate's own `targetDirs` / `ignoreGlobs` off the
235
+ * normalized quality block — the same two keys its refresh scorer reads.
236
+ *
237
+ * @param {{ kind: string, cwd: string, gate: object }} params
238
+ * @returns {{ files: string[] | null, roots: string[], degraded: boolean, reason: string | null }}
239
+ */
240
+ function targetDirsInventory({ kind, cwd, gate }) {
241
+ const roots = Array.isArray(gate?.targetDirs) ? gate.targetDirs : [];
242
+ const ignoreGlobs = Array.isArray(gate?.ignoreGlobs) ? gate.ignoreGlobs : [];
243
+ if (roots.length === 0) {
244
+ return {
245
+ files: null,
246
+ roots: [],
247
+ degraded: true,
248
+ reason: `delivery.quality.gates.${kind}.targetDirs is empty or unset`,
249
+ };
250
+ }
251
+ return {
252
+ files: walkRoots({ cwd, roots, ignoreGlobs }),
253
+ roots,
254
+ degraded: false,
255
+ reason: null,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Resolve the set of files `kind` is configured to have rows for.
261
+ *
262
+ * Never throws: an unreadable or absent scope config degrades to
263
+ * `{ files: null, degraded: true, reason }`. Callers treat a degraded
264
+ * inventory as "scope unknown" — the gate reports it and skips that kind's
265
+ * scope directions, the pruner falls back to orphan-only pruning. Neither may
266
+ * infer emptiness from it: a `files: []` inventory would make every committed
267
+ * row look out-of-scope and hand the pruner a licence to delete the baseline.
268
+ *
269
+ * @param {{
270
+ * kind: string,
271
+ * cwd?: string,
272
+ * quality?: object,
273
+ * requireFn?: (id: string) => object,
274
+ * }} params
275
+ * @returns {{
276
+ * kind: string,
277
+ * keyField: string,
278
+ * directions: readonly string[],
279
+ * files: string[] | null,
280
+ * roots: string[],
281
+ * degraded: boolean,
282
+ * reason: string | null,
283
+ * }}
284
+ */
285
+ export function buildScopeInventory({
286
+ kind,
287
+ cwd = process.cwd(),
288
+ quality,
289
+ requireFn,
290
+ } = {}) {
291
+ const base = {
292
+ kind,
293
+ keyField: keyFieldFor(kind),
294
+ directions: directionsFor(kind),
295
+ };
296
+ if (!isFileKeyed(kind)) {
297
+ return {
298
+ ...base,
299
+ files: null,
300
+ roots: [],
301
+ degraded: false,
302
+ reason: `${kind} is not file-keyed (rows key on "${base.keyField}")`,
303
+ };
304
+ }
305
+ const resolved =
306
+ kind === 'coverage'
307
+ ? coverageInventory({ cwd, requireFn })
308
+ : targetDirsInventory({
309
+ kind,
310
+ cwd,
311
+ gate: quality?.gates?.[kind] ?? {},
312
+ });
313
+ return { ...base, ...resolved };
314
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * bdd-step-index.js — scoped discovery and matching for BDD step definitions.
3
+ *
4
+ * `check-gherkin-corpus.js` answers one question per Gherkin step: does a step
5
+ * definition **under this step's own scope** claim it? That question has two
6
+ * halves, and this module owns both so the corpus gate never re-implements
7
+ * either inline:
8
+ *
9
+ * 1. **Discovery** — which files under a scope's `stepRoots` hold step
10
+ * definitions, and which `.feature` files sit under its `featureRoots`.
11
+ * Feature discovery is delegated to `listFeatureFiles` in
12
+ * `bdd-scenario-scanner.js` rather than copied: /plan's scenario index and
13
+ * this gate must agree on what counts as a feature file, and two walkers
14
+ * would eventually disagree.
15
+ * 2. **Matching** — turn each definition's Cucumber expression or regular
16
+ * expression into one `RegExp`, and test a step's text against the index.
17
+ *
18
+ * ## Heuristic index, exact parser
19
+ *
20
+ * The parser half of the gate is exact: `@cucumber/gherkin` decides what
21
+ * compiles. This half is deliberately **not**. Reading step definitions
22
+ * without executing them means a regex scan over source text, and a scan
23
+ * cannot see a definition assembled at runtime, registered through a wrapper,
24
+ * or parameterised by a custom `defineParameterType`. That asymmetry is why
25
+ * the gate ships a step-waiver list: a false "unbound" must always have an
26
+ * escape that does not require switching the whole gate off.
27
+ *
28
+ * Supported Cucumber-expression constructs are the ones the built-in parameter
29
+ * types and the optional/alternation syntax cover — `{int}`, `{float}`,
30
+ * `{word}`, `{string}`, an anonymous `{}`, `text(s)` optionals, and `a/an`
31
+ * word alternation. An unrecognised `{custom}` degrades to `(.*)` rather than
32
+ * failing to match, because over-matching produces a missed finding while
33
+ * under-matching produces a false one, and a false one blocks a delivery.
34
+ *
35
+ * Nothing here reads configuration or exits a process; the CLI owns both.
36
+ */
37
+
38
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
39
+ import path from 'node:path';
40
+
41
+ import { listFeatureFiles } from './bdd-scenario-scanner.js';
42
+
43
+ export { listFeatureFiles };
44
+
45
+ /** Extensions a step-definition module may carry. */
46
+ const STEP_FILE_EXTENSIONS = Object.freeze([
47
+ '.js',
48
+ '.mjs',
49
+ '.cjs',
50
+ '.ts',
51
+ '.mts',
52
+ '.cts',
53
+ '.tsx',
54
+ ]);
55
+
56
+ /** Directory names never walked when looking for step definitions. */
57
+ const SKIPPED_DIRECTORIES = Object.freeze(
58
+ new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage']),
59
+ );
60
+
61
+ /**
62
+ * Regular expressions for the built-in Cucumber parameter types, keyed by the
63
+ * name inside the braces. The empty key is the anonymous `{}` parameter and
64
+ * doubles as the fallback for a custom type this scan cannot resolve.
65
+ *
66
+ * @type {Readonly<Record<string, string>>}
67
+ */
68
+ const PARAMETER_PATTERNS = Object.freeze({
69
+ '': '(.*)',
70
+ int: '(-?\\d+)',
71
+ float: '(-?\\d*\\.?\\d+)',
72
+ word: '([^\\s]+)',
73
+ string: '("[^"]*"|\'[^\']*\')',
74
+ });
75
+
76
+ /**
77
+ * Step-registration call sites this scan recognises. `Step` and `defineStep`
78
+ * cover the generic registrars playwright-bdd and cucumber-js both expose.
79
+ *
80
+ * Group 2/3 capture a quoted expression, group 4/5 a regular-expression
81
+ * literal with its flags.
82
+ */
83
+ const STEP_CALL_PATTERN =
84
+ /\b(Given|When|Then|And|But|Step|defineStep)\s*\(\s*(?:(['"`])((?:\\.|(?!\2)[^\\])*)\2|\/((?:\\.|\[(?:\\.|[^\]\\])*\]|[^/\\[])+)\/([dgimsuvy]*))/g;
85
+
86
+ /** Escape one character for literal use inside a regular expression. */
87
+ function escapeRegExp(text) {
88
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
89
+ }
90
+
91
+ /**
92
+ * Render one alternation-free segment of a Cucumber expression: parameter
93
+ * placeholders become capture groups, `(optional)` runs become optional
94
+ * groups, and everything else is escaped literal text.
95
+ *
96
+ * @param {string} segment
97
+ * @returns {string}
98
+ */
99
+ function renderSegment(segment) {
100
+ let out = '';
101
+ let i = 0;
102
+ while (i < segment.length) {
103
+ const ch = segment[i];
104
+ if (ch === '\\' && i + 1 < segment.length) {
105
+ out += escapeRegExp(segment[i + 1]);
106
+ i += 2;
107
+ continue;
108
+ }
109
+ if (ch === '{') {
110
+ const end = segment.indexOf('}', i);
111
+ if (end !== -1) {
112
+ const name = segment.slice(i + 1, end);
113
+ out += PARAMETER_PATTERNS[name] ?? PARAMETER_PATTERNS[''];
114
+ i = end + 1;
115
+ continue;
116
+ }
117
+ }
118
+ if (ch === '(') {
119
+ const end = segment.indexOf(')', i);
120
+ if (end !== -1) {
121
+ out += `(?:${renderSegment(segment.slice(i + 1, end))})?`;
122
+ i = end + 1;
123
+ continue;
124
+ }
125
+ }
126
+ out += escapeRegExp(ch);
127
+ i += 1;
128
+ }
129
+ return out;
130
+ }
131
+
132
+ /**
133
+ * Split a word on unescaped `/` so `a/an` becomes two alternatives. Escaped
134
+ * `\/` stays literal.
135
+ *
136
+ * @param {string} word
137
+ * @returns {string[]}
138
+ */
139
+ function splitAlternation(word) {
140
+ const parts = [];
141
+ let current = '';
142
+ for (let i = 0; i < word.length; i += 1) {
143
+ const ch = word[i];
144
+ if (ch === '\\' && i + 1 < word.length) {
145
+ current += ch + word[i + 1];
146
+ i += 1;
147
+ continue;
148
+ }
149
+ if (ch === '/') {
150
+ parts.push(current);
151
+ current = '';
152
+ continue;
153
+ }
154
+ current += ch;
155
+ }
156
+ parts.push(current);
157
+ return parts;
158
+ }
159
+
160
+ /**
161
+ * Compile a Cucumber expression into an anchored `RegExp`.
162
+ *
163
+ * Alternation is resolved per whitespace-separated word, which is what the
164
+ * real expression grammar does. Resolving it across the whole string instead
165
+ * would turn `I have a/an apple` into `^I have a|an apple$` — two anchored
166
+ * alternatives, neither of them the intended step.
167
+ *
168
+ * @param {string} expression
169
+ * @returns {RegExp}
170
+ */
171
+ function expressionToRegExp(expression) {
172
+ const tokens = String(expression).split(/(\s+)/);
173
+ const body = tokens
174
+ .map((token) => {
175
+ if (token.length === 0) return '';
176
+ if (/^\s+$/.test(token)) return escapeRegExp(token);
177
+ const alternatives = splitAlternation(token);
178
+ const rendered = alternatives.map(renderSegment);
179
+ return rendered.length > 1 ? `(?:${rendered.join('|')})` : rendered[0];
180
+ })
181
+ .join('');
182
+ return new RegExp(`^${body}$`);
183
+ }
184
+
185
+ /**
186
+ * Recursively list step-definition source files under the given roots.
187
+ *
188
+ * Deliberately not `walkFilesByExtension` from `fs-walk.js`, which is the
189
+ * shared walker for the lint surfaces. That one matches a single extension and
190
+ * walks everything below the root, so reusing it here would mean seven passes
191
+ * — one per accepted extension — each of them descending into a `node_modules`
192
+ * a consumer's step root may well contain. It also rethrows every non-ENOENT
193
+ * `readdir` failure, where this walker must skip an unreadable directory: an
194
+ * unreadable *scope* has to surface as "zero step definitions", the gate's
195
+ * fail-closed path, which names the scope and its step roots.
196
+ *
197
+ * @param {string[]} roots absolute or cwd-relative directories
198
+ * @returns {string[]} absolute paths, sorted
199
+ */
200
+ export function listStepFiles(roots) {
201
+ const found = [];
202
+ for (const root of roots ?? []) {
203
+ walkStepDir(path.resolve(root), found);
204
+ }
205
+ return found.sort();
206
+ }
207
+
208
+ function walkStepDir(dir, acc) {
209
+ let entries;
210
+ try {
211
+ entries = readdirSync(dir, { withFileTypes: true });
212
+ } catch {
213
+ return;
214
+ }
215
+ for (const entry of entries) {
216
+ const full = path.join(dir, entry.name);
217
+ // `withFileTypes` reports a symlink as neither file nor directory, so
218
+ // fall back to a stat for those rather than dropping them silently.
219
+ const isDir =
220
+ entry.isDirectory() || (entry.isSymbolicLink() && isDirAt(full));
221
+ if (isDir) {
222
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) walkStepDir(full, acc);
223
+ continue;
224
+ }
225
+ if (STEP_FILE_EXTENSIONS.includes(path.extname(entry.name))) acc.push(full);
226
+ }
227
+ }
228
+
229
+ function isDirAt(target) {
230
+ try {
231
+ return statSync(target).isDirectory();
232
+ } catch {
233
+ return false;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Extract every step registration in one source file.
239
+ *
240
+ * @param {string} source file contents
241
+ * @param {string} file absolute path, recorded on each entry
242
+ * @returns {Array<{ file: string, line: number, source: string, regex: RegExp }>}
243
+ */
244
+ function parseStepDefinitions(source, file) {
245
+ const entries = [];
246
+ STEP_CALL_PATTERN.lastIndex = 0;
247
+ let match = STEP_CALL_PATTERN.exec(source);
248
+ while (match !== null) {
249
+ const [, , , quoted, pattern, flags] = match;
250
+ const line = source.slice(0, match.index).split('\n').length;
251
+ const regex = compileMatcher({ quoted, pattern, flags });
252
+ if (regex) entries.push({ file, line, source: quoted ?? pattern, regex });
253
+ match = STEP_CALL_PATTERN.exec(source);
254
+ }
255
+ return entries;
256
+ }
257
+
258
+ /**
259
+ * Build one matcher from a captured registration. A malformed regular
260
+ * expression yields `null` — the definition is skipped rather than crashing
261
+ * the scan, and the steps it would have claimed surface as unbound, which is
262
+ * the safe direction.
263
+ */
264
+ function compileMatcher({ quoted, pattern, flags }) {
265
+ if (typeof quoted === 'string') {
266
+ try {
267
+ return expressionToRegExp(quoted);
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
272
+ try {
273
+ // `g` and `y` are stateful across `.test()` calls; strip them so the index
274
+ // cannot depend on how many times it has been consulted.
275
+ return new RegExp(pattern, (flags ?? '').replace(/[gy]/g, ''));
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Build the step index for one scope.
283
+ *
284
+ * @param {{ files: string[], readFile?: (p: string) => string }} params
285
+ * @returns {{ entries: Array<{ file: string, line: number, source: string, regex: RegExp }>, files: string[] }}
286
+ */
287
+ export function buildStepIndex({ files, readFile }) {
288
+ const read = readFile ?? ((p) => readFileSync(p, 'utf8'));
289
+ const entries = [];
290
+ for (const file of files ?? []) {
291
+ let source;
292
+ try {
293
+ source = read(file);
294
+ } catch {
295
+ continue;
296
+ }
297
+ entries.push(...parseStepDefinitions(source, file));
298
+ }
299
+ return { entries, files: [...(files ?? [])] };
300
+ }
301
+
302
+ /**
303
+ * Find the first definition in the index claiming `text`.
304
+ *
305
+ * @param {{ entries: Array<{ regex: RegExp }> }} index
306
+ * @param {string} text the step text, keyword already stripped by the parser
307
+ * @returns {object | null} the matching entry, or `null` when nothing claims it
308
+ */
309
+ export function matchStep(index, text) {
310
+ for (const entry of index?.entries ?? []) {
311
+ if (entry.regex.test(text)) return entry;
312
+ }
313
+ return null;
314
+ }
315
+
316
+ /**
317
+ * Module-private helpers the suite drives directly. Bundled rather than
318
+ * exported individually — the same seam `knip-entry-sync.js` and
319
+ * `source-classifier.js` use — so test-only symbols cost one production
320
+ * dead-export row instead of one each, and so private helpers do not read as
321
+ * API.
322
+ */
323
+ export const __testing = Object.freeze({
324
+ expressionToRegExp,
325
+ parseStepDefinitions,
326
+ });
@@ -105,9 +105,11 @@ export function ledgerPath(projectRoot) {
105
105
  *
106
106
  * @param {object} args
107
107
  * @param {import('./manifest.js').MutationManifestEntry[]} args.entries
108
- * — the APPROVED subset of the mutation manifest that was applied.
109
- * @param {string[]} args.approvedGroups — the phase groups the operator
110
- * approved (sorted for stable output).
108
+ * — the subset of the mutation manifest that was applied.
109
+ * @param {string[]} args.approvedGroups — the phase groups whose mutations
110
+ * landed (sorted for stable output). Field name predates Story #5007's
111
+ * removal of the phased-approval gate; the ledger record shape is a
112
+ * consumer contract read back by `mandrel uninstall`, so it is unchanged.
111
113
  * @param {{ owner?: string, repo?: string }} [args.answers]
112
114
  * @param {string} [args.appliedAt] — ISO-8601 timestamp (default: now).
113
115
  * @param {Record<string, { action?: string }>} [args.report] — the live
@@ -15,8 +15,7 @@
15
15
  * `HUMAN_INTENT_FIELDS` table here, and each field's heading is the exact
16
16
  * section name the parser maps (`goal` → `## Goal`, etc.). The CI
17
17
  * conformance lint (`lint-issue-body.js`) runs the real `parse()` against
18
- * human-opened issues so the form and the parser cannot silently drift
19
- * the same model `ci-workflow-template.js` follows for `ci.yml`.
18
+ * human-opened issues so the form and the parser cannot silently drift.
20
19
  *
21
20
  * ## Form fields ⊆ body schema
22
21
  *
@@ -70,9 +69,8 @@ export const STORY_FORM_RELATIVE_PATH = `${ISSUE_TEMPLATE_RELATIVE_DIR}/story.ym
70
69
  * generated YAML uses it verbatim as the field `label` so GitHub's
71
70
  * `### {label}` render produces a heading the parser recognises.
72
71
  *
73
- * Machine-managed body fields (`wide`, `reason_to_exist`,
74
- * `estimated_test_files`, `depends_on` meta) are intentionally absent —
75
- * the runtime fills those. `depends_on` is exposed as a free-text input
72
+ * Machine-managed body fields (`wide`, `reason_to_exist`, `depends_on`
73
+ * meta) are intentionally absent — the runtime fills those. `depends_on` is exposed as a free-text input
76
74
  * that serializes to the `blocked by #N` footer `parse()` already reads.
77
75
  *
78
76
  * @type {Array<{
@@ -367,7 +365,7 @@ jobs:
367
365
 
368
366
  /**
369
367
  * Write (or refresh) the Story issue form into a project checkout. Idempotent at
370
- * the byte level — mirrors {@link ensureCiWorkflow}'s contract:
368
+ * the byte level:
371
369
  *
372
370
  * - file absent → `created`
373
371
  * - byte-identical → `unchanged`