mandrel 2.31.0 → 2.32.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 (250) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +4 -4
  4. package/.agents/docs/agentrc-reference.json +61 -57
  5. package/.agents/docs/configuration.md +274 -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 +0 -1
  14. package/.agents/schemas/agentrc.schema.json +1939 -1400
  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 +7 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/bootstrap.js +28 -26
  21. package/.agents/scripts/check-baseline-drift.js +73 -13
  22. package/.agents/scripts/check-baseline-scope.js +362 -0
  23. package/.agents/scripts/check-dead-exports.js +9 -1
  24. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  25. package/.agents/scripts/check-knip-entries.js +136 -0
  26. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  27. package/.agents/scripts/check-schema-references.js +1 -1
  28. package/.agents/scripts/diagnose-friction.js +7 -4
  29. package/.agents/scripts/generate-config-docs.js +263 -171
  30. package/.agents/scripts/install-matrix-assert.js +0 -1
  31. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  32. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  33. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  34. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  35. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  37. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  38. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  39. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  40. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  41. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  42. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  43. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  44. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  45. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  46. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  47. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  48. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  49. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  50. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  51. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  52. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  53. package/.agents/scripts/lib/child-exec.js +193 -0
  54. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  55. package/.agents/scripts/lib/cli-args.js +64 -0
  56. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  57. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  58. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  59. package/.agents/scripts/lib/config/commands.js +3 -5
  60. package/.agents/scripts/lib/config/explain.js +5 -7
  61. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  62. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  63. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  64. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  65. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  66. package/.agents/scripts/lib/config/gates/index.js +5 -2
  67. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  68. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  69. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  70. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  71. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  72. package/.agents/scripts/lib/config/github.js +0 -1
  73. package/.agents/scripts/lib/config/limits.js +3 -48
  74. package/.agents/scripts/lib/config/qa.js +105 -0
  75. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  76. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  77. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  78. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  79. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  80. package/.agents/scripts/lib/crap-utils.js +84 -520
  81. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  82. package/.agents/scripts/lib/degraded-mode.js +2 -2
  83. package/.agents/scripts/lib/doc-tiers.js +3 -3
  84. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  85. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  86. package/.agents/scripts/lib/fs-walk.js +52 -0
  87. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  88. package/.agents/scripts/lib/git-utils.js +16 -36
  89. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  90. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  91. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  92. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  93. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  94. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  95. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  96. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  97. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  98. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  99. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  100. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  101. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  102. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  103. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  104. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  105. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  106. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  107. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  108. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  109. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  110. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  111. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  112. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  122. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  123. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  124. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  125. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  126. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  127. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  128. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  129. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  130. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  131. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  132. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  133. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  134. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  135. package/.agents/scripts/lib/signals/index.js +8 -6
  136. package/.agents/scripts/lib/signals/schema.js +20 -25
  137. package/.agents/scripts/lib/signals/write.js +8 -8
  138. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  139. package/.agents/scripts/lib/temp-retention.js +1 -1
  140. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  141. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  142. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  143. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  144. package/.agents/scripts/lib/worktree-manager.js +2 -3
  145. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  146. package/.agents/scripts/pr-watch-with-update.js +7 -5
  147. package/.agents/scripts/providers/github/cache.js +2 -2
  148. package/.agents/scripts/providers/github/comments.js +6 -28
  149. package/.agents/scripts/providers/github/compose.js +0 -15
  150. package/.agents/scripts/providers/github/errors.js +10 -27
  151. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  152. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  153. package/.agents/scripts/providers/github.js +4 -7
  154. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  155. package/.agents/scripts/resolve-stories.js +0 -2
  156. package/.agents/scripts/run-lint.js +61 -61
  157. package/.agents/scripts/run-test-profile.js +6 -6
  158. package/.agents/scripts/run-verify.js +48 -30
  159. package/.agents/scripts/single-story-close.js +20 -0
  160. package/.agents/scripts/single-story-init.js +12 -35
  161. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  162. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  163. package/.agents/skills/skills.index.json +1 -11
  164. package/.agents/workflows/audit-documentation.md +5 -6
  165. package/.agents/workflows/audit-to-stories.md +2 -2
  166. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  167. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  168. package/.agents/workflows/helpers/code-review.md +3 -8
  169. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  170. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  171. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  172. package/.agents/workflows/mandrel-update.md +10 -10
  173. package/.agents/workflows/qa-assist.md +15 -20
  174. package/.agents/workflows/qa-explore.md +9 -8
  175. package/README.md +1 -1
  176. package/docs/CHANGELOG.md +42 -0
  177. package/lib/migrations/index.js +2 -0
  178. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  179. package/package.json +12 -3
  180. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  181. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  182. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  183. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  184. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  185. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  186. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  187. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  188. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  189. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  190. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  191. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  192. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  193. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  194. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  195. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  196. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  197. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  198. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  199. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  200. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  201. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  202. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  203. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  204. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  205. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  206. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  207. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  208. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  209. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  210. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  211. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  212. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  213. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  214. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  215. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  216. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  217. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  218. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  219. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  220. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  221. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  222. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  223. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  224. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  225. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  226. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  227. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  228. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  229. package/.agents/scripts/lib/planning-corpus.js +0 -37
  230. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  231. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  232. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  233. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  234. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  235. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  236. package/.agents/scripts/lib/signals/read.js +0 -268
  237. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  238. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  239. package/.agents/scripts/lib/story-plan.js +0 -379
  240. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  241. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  242. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  243. package/.agents/scripts/lint-baseline.js +0 -507
  244. package/.agents/scripts/providers/github/prs.js +0 -103
  245. package/.agents/scripts/signals-view.js +0 -309
  246. package/.agents/scripts/story-plan.js +0 -370
  247. package/.agents/scripts/sync-branch-from-base.js +0 -149
  248. package/.agents/scripts/validate-docs-freshness.js +0 -314
  249. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  250. package/.agents/workflows/helpers/signals.md +0 -112
@@ -0,0 +1,508 @@
1
+ #!/usr/bin/env node
2
+
3
+ // .agents/scripts/check-gherkin-corpus.js — static gate over a project's
4
+ // Gherkin corpus: must-compile, then must-bind, scoped per step root.
5
+ //
6
+ // The framework ships the bddgen harness but nothing that inspects the corpus
7
+ // it generates from. Two corpus-wide failures are invisible until generation
8
+ // time, and both take the whole acceptance suite dark at once: a `.feature`
9
+ // the parser rejects, and a step no definition claims. This gate catches both
10
+ // offline, in the same `npm run lint` that already guards every other
11
+ // framework-owned surface.
12
+ //
13
+ // Two contracts make the findings trustworthy rather than merely loud:
14
+ //
15
+ // must-compile parses with the REAL `@cucumber/gherkin` parser. A gate that
16
+ // re-implements acceptance is the defect it is trying to prevent: a
17
+ // hand-rolled line reader silently skips what it does not recognise, so a
18
+ // corpus that cannot generate reads clean. Whatever bddgen accepts is what
19
+ // this gate must accept, and the only way to guarantee that is to run the
20
+ // same parser.
21
+ //
22
+ // A file failing must-compile is EXCLUDED from must-bind. A broken file
23
+ // parses as an arbitrary subset of itself, so linting its surviving steps
24
+ // invents unbound findings that bury the one actionable line — the syntax
25
+ // error — under noise.
26
+ //
27
+ // The parser is an optional peer dependency (plus a framework devDependency),
28
+ // resolved through a require path rooted at the consumer project rather than
29
+ // imported by bare specifier. `.agents/` reaches a consumer by plain file
30
+ // copy, so a bare specifier here would resolve against the consumer's own
31
+ // module chain, which under a non-hoisting linker need not hold it at all.
32
+ // This mirrors the `typescript` optional-peer precedent and keeps a consumer
33
+ // with no BDD tier from gaining a runtime dependency.
34
+ //
35
+ // Exit codes:
36
+ // 0 clean, or `qa.gherkinLint` is not configured
37
+ // 1 a parse error, an unbound step, or a fail-closed condition
38
+
39
+ // .agents/scripts/check-gherkin-corpus.js — static gate over a project's
40
+ // Gherkin corpus: must-compile, then must-bind, scoped per step root.
41
+ //
42
+ // The framework ships the bddgen harness but nothing that inspects the corpus
43
+ // it generates from. Two corpus-wide failures are invisible until generation
44
+ // time, and both take the whole acceptance suite dark at once: a `.feature`
45
+ // the parser rejects, and a step no definition claims. This gate catches both
46
+ // offline, in the same `npm run lint` that already guards every other
47
+ // framework-owned surface.
48
+ //
49
+ // Two contracts make the findings trustworthy rather than merely loud:
50
+ //
51
+ // must-compile parses with the REAL `@cucumber/gherkin` parser. A gate that
52
+ // re-implements acceptance is the defect it is trying to prevent: a
53
+ // hand-rolled line reader silently skips what it does not recognise, so a
54
+ // corpus that cannot generate reads clean. Whatever bddgen accepts is what
55
+ // this gate must accept, and the only way to guarantee that is to run the
56
+ // same parser.
57
+ //
58
+ // A file failing must-compile is EXCLUDED from must-bind. A broken file
59
+ // parses as an arbitrary subset of itself, so linting its surviving steps
60
+ // invents unbound findings that bury the one actionable line — the syntax
61
+ // error — under noise.
62
+ //
63
+ // The parser is an optional peer dependency (plus a framework devDependency),
64
+ // resolved through a require path rooted at the consumer project rather than
65
+ // imported by bare specifier. `.agents/` reaches a consumer by plain file
66
+ // copy, so a bare specifier here would resolve against the consumer's own
67
+ // module chain, which under a non-hoisting linker need not hold it at all.
68
+ // This mirrors the `typescript` optional-peer precedent and keeps a consumer
69
+ // with no BDD tier from gaining a runtime dependency.
70
+ //
71
+ // Exit codes:
72
+ // 0 clean, or `qa.gherkinLint` is not configured
73
+ // 1 a parse error, an unbound step, or a fail-closed condition
74
+
75
+ import fs, { readFileSync } from 'node:fs';
76
+ import { createRequire } from 'node:module';
77
+ import path from 'node:path';
78
+ import process from 'node:process';
79
+ import { pathToFileURL } from 'node:url';
80
+
81
+ import {
82
+ buildStepIndex,
83
+ listFeatureFiles,
84
+ listStepFiles,
85
+ matchStep,
86
+ } from './lib/bdd-step-index.js';
87
+ import { runAsCli } from './lib/cli-utils.js';
88
+ import { getGherkinLint } from './lib/config/qa.js';
89
+ import { resolveConfig } from './lib/config-resolver.js';
90
+
91
+ const TAG = '[gherkin-corpus]';
92
+ const PARSER_PACKAGE = '@cucumber/gherkin';
93
+
94
+ const HELP = {
95
+ invocation: 'node .agents/scripts/check-gherkin-corpus.js [--cwd <dir>]',
96
+ summary:
97
+ 'Static gate over the Gherkin corpus: every in-scope .feature must compile with the real parser, and every active step must bind to a definition under its own scope.',
98
+ flags: [['--cwd <dir>', 'Project root to check. Default: process.cwd().']],
99
+ notes: [
100
+ 'Opt-in: the gate runs only when `qa.gherkinLint` is configured in .agentrc.json.',
101
+ 'Escapes for a false unbound: `exemptionTags` (default ["@skip"]) and `stepWaivers`.',
102
+ 'Exit codes:\n 0 clean, or not configured\n 1 parse error, unbound step, or a fail-closed condition',
103
+ ],
104
+ };
105
+
106
+ /**
107
+ * Parse argv into an options bag.
108
+ *
109
+ * @param {string[]} argv
110
+ * @returns {{ cwd: string | null }}
111
+ */
112
+ export function parseArgs(argv = []) {
113
+ const out = { cwd: null };
114
+ for (let i = 0; i < argv.length; i += 1) {
115
+ if (argv[i] === '--cwd') {
116
+ const next = argv[i + 1];
117
+ if (next && !next.startsWith('--')) {
118
+ out.cwd = next;
119
+ i += 1;
120
+ }
121
+ }
122
+ }
123
+ return out;
124
+ }
125
+
126
+ /**
127
+ * Resolve `@cucumber/gherkin` through a require path anchored at the project
128
+ * being checked, falling back to the framework's own installation.
129
+ *
130
+ * @param {{ cwd: string }} params
131
+ * @returns {Promise<{ parse: (source: string) => object }>}
132
+ * @throws {Error} when the package cannot be resolved from either anchor
133
+ */
134
+ export async function loadGherkinParser({ cwd }) {
135
+ const anchors = [
136
+ createRequire(path.join(path.resolve(cwd), 'noop.cjs')),
137
+ createRequire(import.meta.url),
138
+ ];
139
+ let resolved = null;
140
+ for (const anchor of anchors) {
141
+ try {
142
+ resolved = anchor.resolve(PARSER_PACKAGE);
143
+ break;
144
+ } catch {
145
+ // Try the next anchor; the aggregate failure is reported by the caller.
146
+ }
147
+ }
148
+ if (!resolved) {
149
+ throw new Error(
150
+ `${PARSER_PACKAGE} could not be resolved from ${path.resolve(cwd)} or from the framework's own install — install it with \`npm install --save-dev ${PARSER_PACKAGE}\``,
151
+ );
152
+ }
153
+ const { AstBuilder, GherkinClassicTokenMatcher, Parser } = await import(
154
+ pathToFileURL(resolved).href
155
+ );
156
+ return {
157
+ parse(source) {
158
+ // The id counter is supplied locally so `@cucumber/messages`
159
+ // (`IdGenerator.uuid`) is not a second import to resolve.
160
+ let seq = 0;
161
+ const nextId = () => {
162
+ seq += 1;
163
+ return String(seq);
164
+ };
165
+ const parser = new Parser(
166
+ new AstBuilder(nextId),
167
+ new GherkinClassicTokenMatcher(),
168
+ );
169
+ return parser.parse(source);
170
+ },
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Normalize a parser exception into one finding per reported position.
176
+ *
177
+ * @param {unknown} error
178
+ * @param {string} file repo-relative path
179
+ * @returns {Array<{ kind: 'parse-error', file: string, line: number, column: number, message: string }>}
180
+ */
181
+ export function toParseFindings(error, file) {
182
+ const reported = Array.isArray(error?.errors) ? error.errors : [error];
183
+ return reported.map((entry) => ({
184
+ kind: 'parse-error',
185
+ file,
186
+ line: entry?.location?.line ?? 1,
187
+ column: entry?.location?.column ?? 1,
188
+ message: String(entry?.message ?? entry),
189
+ }));
190
+ }
191
+
192
+ /** Tag names carried by an AST node, as a plain string array. */
193
+ function tagNames(node) {
194
+ return (node?.tags ?? []).map((tag) => tag.name);
195
+ }
196
+
197
+ /**
198
+ * Expand one step's text against an Examples table. A step with no `<param>`
199
+ * placeholder yields itself; an outline step yields one variant per row.
200
+ *
201
+ * @param {string} text
202
+ * @param {Array<{ tableHeader?: object, tableBody?: object[] }>} examples
203
+ * @returns {string[]}
204
+ */
205
+ export function expandStepText(text, examples) {
206
+ if (!text.includes('<')) return [text];
207
+ const variants = [];
208
+ for (const example of examples ?? []) {
209
+ const headers = (example?.tableHeader?.cells ?? []).map((c) => c.value);
210
+ for (const row of example?.tableBody ?? []) {
211
+ const cells = (row?.cells ?? []).map((c) => c.value);
212
+ let expanded = text;
213
+ headers.forEach((header, i) => {
214
+ expanded = expanded.split(`<${header}>`).join(cells[i] ?? '');
215
+ });
216
+ variants.push(expanded);
217
+ }
218
+ }
219
+ return variants.length > 0 ? variants : [text];
220
+ }
221
+
222
+ /**
223
+ * Flatten a compiled feature into the steps must-bind is responsible for.
224
+ *
225
+ * Background steps run for every scenario in their container, so they are
226
+ * checked once — but only when the container still holds a non-exempt
227
+ * scenario, otherwise exempting every scenario in a file would leave its
228
+ * background as the sole remaining source of findings.
229
+ *
230
+ * @param {object} feature the `document.feature` node
231
+ * @param {string[]} exemptionTags
232
+ * @returns {Array<{ line: number, text: string, examples: object[] }>}
233
+ */
234
+ export function collectActiveSteps(feature, exemptionTags) {
235
+ const exempt = new Set(exemptionTags);
236
+ const featureTags = tagNames(feature);
237
+ const steps = [];
238
+
239
+ // A background step is reachable from more than one container once Rules are
240
+ // in play, so emitting is keyed on the step's own line to check it exactly
241
+ // once per feature rather than once per container that inherits it.
242
+ const emittedBackgroundLines = new Set();
243
+ const pushBackgroundSteps = (backgrounds) => {
244
+ for (const background of backgrounds) {
245
+ for (const step of background.steps ?? []) {
246
+ if (emittedBackgroundLines.has(step.location.line)) continue;
247
+ emittedBackgroundLines.add(step.location.line);
248
+ steps.push({ line: step.location.line, text: step.text, examples: [] });
249
+ }
250
+ }
251
+ };
252
+
253
+ const walkContainer = (container, inheritedTags, inheritedBackgrounds) => {
254
+ const backgrounds = [...inheritedBackgrounds];
255
+ const scenarios = [];
256
+ for (const child of container?.children ?? []) {
257
+ if (child.background) backgrounds.push(child.background);
258
+ if (child.scenario) scenarios.push(child.scenario);
259
+ }
260
+ // Gherkin runs a feature-level Background for every scenario in the
261
+ // feature, Rule-nested ones included, so the rule walk inherits the
262
+ // backgrounds collected above. Recursing before the no-active-scenarios
263
+ // return is what lets a feature whose scenarios all live under Rules still
264
+ // have its Background checked.
265
+ for (const child of container?.children ?? []) {
266
+ if (!child.rule) continue;
267
+ walkContainer(
268
+ child.rule,
269
+ [...inheritedTags, ...tagNames(child.rule)],
270
+ backgrounds,
271
+ );
272
+ }
273
+ const active = scenarios.filter(
274
+ (scenario) =>
275
+ ![...inheritedTags, ...tagNames(scenario)].some((tag) =>
276
+ exempt.has(tag),
277
+ ),
278
+ );
279
+ if (active.length === 0) return;
280
+ pushBackgroundSteps(backgrounds);
281
+ for (const scenario of active) {
282
+ for (const step of scenario.steps ?? []) {
283
+ steps.push({
284
+ line: step.location.line,
285
+ text: step.text,
286
+ examples: scenario.examples ?? [],
287
+ });
288
+ }
289
+ }
290
+ };
291
+
292
+ walkContainer(feature, featureTags, []);
293
+ return steps;
294
+ }
295
+
296
+ /**
297
+ * Score one feature file's active steps against a scope's step index.
298
+ *
299
+ * A step counts as bound when **any** of its Examples expansions matches. The
300
+ * conservative direction is deliberate: a partially-binding outline step is
301
+ * almost always a parameter-type mismatch in the index, and reporting it would
302
+ * spend the operator's attention on the heuristic rather than on the corpus.
303
+ *
304
+ * @returns {Array<{ kind: 'unbound', file: string, line: number, text: string, scope: string }>}
305
+ */
306
+ function bindFindings({ feature, file, scope, index, exemptionTags, waivers }) {
307
+ const findings = [];
308
+ const seen = new Set();
309
+ for (const step of collectActiveSteps(feature, exemptionTags)) {
310
+ if (waivers.has(step.text)) continue;
311
+ const key = `${step.line}|${step.text}`;
312
+ if (seen.has(key)) continue;
313
+ seen.add(key);
314
+ const variants = expandStepText(step.text, step.examples);
315
+ if (variants.some((variant) => matchStep(index, variant))) continue;
316
+ findings.push({
317
+ kind: 'unbound',
318
+ file,
319
+ line: step.line,
320
+ text: step.text,
321
+ scope: scope.name,
322
+ });
323
+ }
324
+ return findings;
325
+ }
326
+
327
+ /**
328
+ * Run must-compile then must-bind across one scope.
329
+ *
330
+ * @returns {{ findings: object[], featureCount: number, stepDefinitionCount: number }}
331
+ */
332
+ export function scanScope({
333
+ scope,
334
+ cwd,
335
+ parser,
336
+ exemptionTags,
337
+ waivers,
338
+ readFile,
339
+ }) {
340
+ const featureFiles = listFeatureFiles(
341
+ scope.featureRoots.map((root) => path.resolve(cwd, root)),
342
+ ).sort();
343
+ const stepFiles = listStepFiles(
344
+ scope.stepRoots.map((root) => path.resolve(cwd, root)),
345
+ );
346
+ const index = buildStepIndex({ files: stepFiles, readFile });
347
+ const findings = [];
348
+
349
+ for (const absolute of featureFiles) {
350
+ const file = path.relative(cwd, absolute);
351
+ let document;
352
+ try {
353
+ document = parser.parse(
354
+ readFile ? readFile(absolute) : readFileSync(absolute, 'utf8'),
355
+ );
356
+ } catch (error) {
357
+ // Excluded from must-bind: a broken file parses as an arbitrary subset
358
+ // of itself, and its invented findings would bury this one.
359
+ findings.push(...toParseFindings(error, file));
360
+ continue;
361
+ }
362
+ if (!document?.feature) continue;
363
+ findings.push(
364
+ ...bindFindings({
365
+ feature: document.feature,
366
+ file,
367
+ scope,
368
+ index,
369
+ exemptionTags,
370
+ waivers,
371
+ }),
372
+ );
373
+ }
374
+
375
+ return {
376
+ findings,
377
+ featureCount: featureFiles.length,
378
+ stepDefinitionCount: index.entries.length,
379
+ };
380
+ }
381
+
382
+ /**
383
+ * The featureRoots half of the blackout contract the stepRoots check already
384
+ * covers on the definitions side. A renamed or typo'd root resolves zero
385
+ * features, and "nothing to check" would then report green over a corpus
386
+ * nobody is checking. A root that exists but holds no `.feature` file yet is
387
+ * the legitimate not-written-them-yet case and stays passing — absence of the
388
+ * directory is what separates misconfiguration from an empty corpus.
389
+ *
390
+ * @param {Array<{ name: string, featureRoots: string[] }>} scopes
391
+ * @param {string} root
392
+ * @returns {string | null} the operator-facing message, or null when clean
393
+ */
394
+ function findMissingFeatureRoots(scopes, root) {
395
+ for (const scope of scopes) {
396
+ const missing = scope.featureRoots.filter(
397
+ (r) => !fs.existsSync(path.resolve(root, r)),
398
+ );
399
+ if (missing.length === 0) continue;
400
+ return (
401
+ `scope "${scope.name}" names featureRoots that do not exist: ${missing.join(', ')} — ` +
402
+ 'every feature in this scope would go unchecked, which is a blackout, ' +
403
+ "not a clean run. Point featureRoots at the directory holding this scope's .feature files."
404
+ );
405
+ }
406
+ return null;
407
+ }
408
+
409
+ /** Render one finding as a single operator-readable line. */
410
+ export function renderFinding(finding) {
411
+ if (finding.kind === 'parse-error') {
412
+ return `${TAG} parse-error ${finding.file}:${finding.line}:${finding.column} ${finding.message}`;
413
+ }
414
+ return `${TAG} unbound [${finding.scope}] ${finding.file}:${finding.line} ${finding.text}`;
415
+ }
416
+
417
+ /**
418
+ * CLI body. Exported so the suite can drive the whole pipeline against fixture
419
+ * projects, including the unresolvable-parser path via `loadParser`.
420
+ *
421
+ * @returns {Promise<number>} process exit code
422
+ */
423
+ export async function runCli({
424
+ argv = process.argv.slice(2),
425
+ cwd = process.cwd(),
426
+ stdout = process.stdout,
427
+ stderr = process.stderr,
428
+ loadParser = loadGherkinParser,
429
+ readFile,
430
+ } = {}) {
431
+ const args = parseArgs(argv);
432
+ const root = path.resolve(args.cwd ?? cwd);
433
+ const config = resolveConfig({ cwd: root, bustCache: true });
434
+ const contract = getGherkinLint(config);
435
+
436
+ if (!contract) {
437
+ stdout.write(
438
+ `${TAG} not configured — add a \`qa.gherkinLint\` block to .agentrc.json to enable this gate.\n`,
439
+ );
440
+ return 0;
441
+ }
442
+
443
+ const blackout = findMissingFeatureRoots(contract.scopes, root);
444
+ if (blackout) {
445
+ stderr.write(`${TAG} ❌ ${blackout}\n`);
446
+ return 1;
447
+ }
448
+
449
+ const corpusSize = contract.scopes.reduce(
450
+ (total, scope) =>
451
+ total +
452
+ listFeatureFiles(scope.featureRoots.map((r) => path.resolve(root, r)))
453
+ .length,
454
+ 0,
455
+ );
456
+ if (corpusSize === 0) {
457
+ stdout.write(`${TAG} no .feature files in scope — nothing to check.\n`);
458
+ return 0;
459
+ }
460
+
461
+ let parser;
462
+ try {
463
+ parser = await loadParser({ cwd: root });
464
+ } catch (error) {
465
+ stderr.write(`${TAG} ❌ ${error.message}\n`);
466
+ return 1;
467
+ }
468
+
469
+ const waivers = new Set(contract.stepWaivers);
470
+ const findings = [];
471
+ let checked = 0;
472
+
473
+ for (const scope of contract.scopes) {
474
+ const result = scanScope({
475
+ scope,
476
+ cwd: root,
477
+ parser,
478
+ exemptionTags: contract.exemptionTags,
479
+ waivers,
480
+ readFile,
481
+ });
482
+ if (result.featureCount > 0 && result.stepDefinitionCount === 0) {
483
+ stderr.write(
484
+ `${TAG} ❌ scope "${scope.name}" resolved 0 step definitions from stepRoots: ${scope.stepRoots.join(', ')} — every step would report unbound, which is a blackout, not a finding. Point stepRoots at the directory holding this scope's step definitions.\n`,
485
+ );
486
+ return 1;
487
+ }
488
+ checked += result.featureCount;
489
+ findings.push(...result.findings);
490
+ }
491
+
492
+ if (findings.length === 0) {
493
+ stdout.write(`${TAG} ✅ ${checked} feature file(s) compile and bind.\n`);
494
+ return 0;
495
+ }
496
+ for (const finding of findings) stderr.write(`${renderFinding(finding)}\n`);
497
+ stderr.write(
498
+ `${TAG} ❌ ${findings.length} finding(s) across ${checked} feature file(s).\n`,
499
+ );
500
+ return 1;
501
+ }
502
+
503
+ runAsCli(import.meta.url, () => runCli(), {
504
+ source: 'gherkin-corpus',
505
+ propagateExitCode: true,
506
+ errorPrefix: `${TAG} ❌ Fatal error`,
507
+ usage: HELP,
508
+ });
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+
3
+ // .agents/scripts/check-knip-entries.js — the guard #5001 left unbuilt.
4
+ //
5
+ // Story #5001 made two changes that are individually right and jointly unsafe:
6
+ // it replaced knip's blanket `.agents/scripts/*.js!` entry glob with an
7
+ // explicit list (so an uninvoked CLI surfaces as dead), and it promoted knip's
8
+ // `files` rule to `error` (so whole-file death produces baseline rows). The
9
+ // explicit list is hand-maintained; nothing checked it.
10
+ //
11
+ // So any CLI added after #5001 is, by default, invisible to knip: absent from
12
+ // the entry list, it reads as unreachable, emits a `{ file, symbol: '*' }` row,
13
+ // and drags every lib module only it imports into the dead set with it. Story
14
+ // #5012 hit exactly this — 5 false rows — and the ratchet's natural remedy
15
+ // (accept the diff) would have written live operator CLIs into
16
+ // `baselines/dead-exports-production.json` as expected-dead, permanently. It
17
+ // was caught by luck: a base-sync conflict forced a manual read of the diff.
18
+ //
19
+ // This gate removes the luck. It derives the invoked set from the executable
20
+ // surfaces #5001's own acceptance criterion named — package.json scripts, husky
21
+ // hooks, `.github/workflows`, `.agents` workflow/skill/agent/rule markdown, and
22
+ // script-to-script spawns — and asserts it matches `knip.json`'s entry array in
23
+ // both directions. See `lib/knip-entry-sync.js` for why liveness means
24
+ // *invoked* rather than *present*, and why documentation prose does not count.
25
+ //
26
+ // Measurement-free by construction: no knip spawn, no scorer, no coverage
27
+ // artifact. It is a directory read and a handful of regexes, which is what
28
+ // makes it cheap enough to sit in the required-check set next to
29
+ // `check-baseline-scope.js`.
30
+ //
31
+ // Exit codes:
32
+ // 0 entry list matches the invoked set
33
+ // 1 divergence — a missing, stale, or phantom entry
34
+ // 2 the check could not run (unreadable knip.json, unusable repository)
35
+
36
+ import process from 'node:process';
37
+ import { runAsCli } from './lib/cli-utils.js';
38
+ import {
39
+ countDivergences,
40
+ renderEntrySyncReport,
41
+ resolveEntrySync,
42
+ } from './lib/knip-entry-sync.js';
43
+
44
+ const EXIT_PASS = 0;
45
+ const EXIT_DIVERGED = 1;
46
+ const EXIT_CANNOT_RUN = 2;
47
+
48
+ const HELP = {
49
+ invocation:
50
+ 'node .agents/scripts/check-knip-entries.js [--cwd <dir>] [--json]',
51
+ summary:
52
+ "Assert knip.json's explicit .agents/scripts entry list matches the set of CLIs something actually invokes.",
53
+ flags: [
54
+ ['--cwd <dir>', 'Repository root to check. Default: process.cwd().'],
55
+ ['--json', 'Emit the report as JSON instead of text.'],
56
+ ],
57
+ notes: [
58
+ 'Exit codes:\n 0 entry list matches\n 1 divergence\n 2 the check could not run',
59
+ 'A missing entry is the dangerous one: knip calls the CLI dead, and accepting\nthe dead-exports diff would record a live CLI as expected-dead (Story #5012).',
60
+ ],
61
+ };
62
+
63
+ /**
64
+ * Parse argv into an options bag. An unknown flag is a config error rather than
65
+ * a silent no-op, matching `check-baseline-scope.js`.
66
+ *
67
+ * @param {string[]} argv
68
+ * @returns {{ cwd: string | null, json: boolean }}
69
+ */
70
+ export function parseArgs(argv = []) {
71
+ const out = { cwd: null, json: false };
72
+ let i = 0;
73
+ while (i < argv.length) {
74
+ const arg = argv[i];
75
+ i += 1;
76
+ if (arg === '--json') out.json = true;
77
+ else if (arg === '--cwd') {
78
+ out.cwd = argv[i];
79
+ i += 1;
80
+ } else throw new Error(`unknown flag "${arg}" (try --help)`);
81
+ }
82
+ return out;
83
+ }
84
+
85
+ /**
86
+ * Top-level CLI entry. Exported so tests can drive it against a fixture tree
87
+ * without spawning a process.
88
+ *
89
+ * @param {{
90
+ * argv?: string[],
91
+ * cwd?: string,
92
+ * stdout?: { write: (s: string) => void },
93
+ * stderr?: { write: (s: string) => void },
94
+ * }} [opts]
95
+ * @returns {Promise<number>} 0 pass, 1 divergence, 2 cannot run
96
+ */
97
+ export async function runCli({
98
+ argv = process.argv.slice(2),
99
+ cwd = process.cwd(),
100
+ stdout = process.stdout,
101
+ stderr = process.stderr,
102
+ } = {}) {
103
+ let args;
104
+ try {
105
+ args = parseArgs(argv);
106
+ } catch (err) {
107
+ stderr.write(`[knip-entries] ❌ ${err?.message ?? String(err)}\n`);
108
+ return EXIT_CANNOT_RUN;
109
+ }
110
+ const repoRoot = args.cwd ?? cwd;
111
+ const report = resolveEntrySync({ repoRoot });
112
+
113
+ if (args.json) {
114
+ stdout.write(
115
+ `${JSON.stringify({ kind: 'knip-entry-sync', ...report }, null, 2)}\n`,
116
+ );
117
+ if (report.error) return EXIT_CANNOT_RUN;
118
+ return countDivergences(report) > 0 ? EXIT_DIVERGED : EXIT_PASS;
119
+ }
120
+
121
+ if (report.error) {
122
+ stderr.write(`[knip-entries] ❌ ${report.error}\n`);
123
+ return EXIT_CANNOT_RUN;
124
+ }
125
+
126
+ stdout.write(`\n--- knip-entries ---\n`);
127
+ stdout.write(`${renderEntrySyncReport(report)}\n`);
128
+ return countDivergences(report) > 0 ? EXIT_DIVERGED : EXIT_PASS;
129
+ }
130
+
131
+ runAsCli(import.meta.url, async () => runCli(), {
132
+ source: 'knip-entries',
133
+ propagateExitCode: true,
134
+ errorPrefix: '[knip-entries] ❌ Fatal error',
135
+ usage: HELP,
136
+ });