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,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,159 @@
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 the entry array of whatever
23
+ // configuration knip itself would load, in both directions. See
24
+ // `lib/knip-entry-sync.js` for why liveness means *invoked* rather than
25
+ // *present*, and why documentation prose does not count.
26
+ //
27
+ // Analysis-free by construction: knip's config resolver is loaded (a
28
+ // `knip.config.ts` has to be evaluated, not parsed), but nothing is scanned —
29
+ // no knip run, no scorer, no coverage artifact. Past that it is a directory
30
+ // read and a handful of regexes, which is what keeps it cheap enough to sit in
31
+ // the required-check set next to `check-baseline-scope.js`.
32
+ //
33
+ // Not-applicable is not failure (Story #5039). A repository with no knip
34
+ // configuration at all — and one where `knip` is not installed — exits 0 with a
35
+ // skip line, the same opt-in posture `qa.gherkinLint` uses. Without that, the
36
+ // gate could not be wired into a consumer that does not run knip, and #5001's
37
+ // guard stayed unbuilt everywhere it was most needed. A configuration that
38
+ // EXISTS but cannot be resolved still exits 2: absence and breakage are
39
+ // different answers.
40
+ //
41
+ // Exit codes:
42
+ // 0 entry list matches the invoked set, or there is no configuration to check
43
+ // 1 divergence — a missing, stale, phantom, or unsuffixed entry
44
+ // 2 the check could not run (unresolvable configuration, unusable repository)
45
+
46
+ import process from 'node:process';
47
+ import { runAsCli } from './lib/cli-utils.js';
48
+ import {
49
+ countDivergences,
50
+ renderEntrySyncReport,
51
+ resolveEntrySync,
52
+ } from './lib/knip-entry-sync.js';
53
+
54
+ const EXIT_PASS = 0;
55
+ const EXIT_DIVERGED = 1;
56
+ const EXIT_CANNOT_RUN = 2;
57
+
58
+ const HELP = {
59
+ invocation:
60
+ 'node .agents/scripts/check-knip-entries.js [--cwd <dir>] [--json]',
61
+ summary:
62
+ "Assert the explicit .agents/scripts entry list in knip's resolved configuration matches the set of CLIs something actually invokes.",
63
+ flags: [
64
+ ['--cwd <dir>', 'Repository root to check. Default: process.cwd().'],
65
+ ['--json', 'Emit the report as JSON instead of text.'],
66
+ ],
67
+ notes: [
68
+ 'Resolves the config through knip itself, so every location knip supports works\n(knip.json/.jsonc, .knip.json(c), knip.ts/.js, knip.config.ts/.js,\npackage.json#knip). A TS config is evaluated, so a computed entry array reads\ncorrectly, and per-workspace entries count alongside the top-level array.',
69
+ 'Exit codes:\n 0 entry list matches, or there is no config to check (skip)\n 1 divergence\n 2 the check could not run — a config that EXISTS but will not resolve',
70
+ '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).',
71
+ ],
72
+ };
73
+
74
+ /**
75
+ * The gate's single exit-code decision, shared by the text and `--json` paths.
76
+ *
77
+ * Kept in one place for the same reason `countDivergences` is: a direction
78
+ * added to the report must not be able to change one path's verdict and not
79
+ * the other's. Skip outranks error outranks divergence — a repository with no
80
+ * configuration has nothing to diverge from.
81
+ *
82
+ * @param {Awaited<ReturnType<typeof resolveEntrySync>>} report
83
+ * @returns {number}
84
+ */
85
+ function exitCodeFor(report) {
86
+ if (report.skipped) return EXIT_PASS;
87
+ if (report.error) return EXIT_CANNOT_RUN;
88
+ return countDivergences(report) > 0 ? EXIT_DIVERGED : EXIT_PASS;
89
+ }
90
+
91
+ /**
92
+ * Parse argv into an options bag. An unknown flag is a config error rather than
93
+ * a silent no-op, matching `check-baseline-scope.js`.
94
+ *
95
+ * @param {string[]} argv
96
+ * @returns {{ cwd: string | null, json: boolean }}
97
+ */
98
+ export function parseArgs(argv = []) {
99
+ const out = { cwd: null, json: false };
100
+ let i = 0;
101
+ while (i < argv.length) {
102
+ const arg = argv[i];
103
+ i += 1;
104
+ if (arg === '--json') out.json = true;
105
+ else if (arg === '--cwd') {
106
+ out.cwd = argv[i];
107
+ i += 1;
108
+ } else throw new Error(`unknown flag "${arg}" (try --help)`);
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /**
114
+ * Top-level CLI entry. Exported so tests can drive it against a fixture tree
115
+ * without spawning a process.
116
+ *
117
+ * @param {{
118
+ * argv?: string[],
119
+ * cwd?: string,
120
+ * stdout?: { write: (s: string) => void },
121
+ * stderr?: { write: (s: string) => void },
122
+ * }} [opts]
123
+ * @returns {Promise<number>} 0 pass, 1 divergence, 2 cannot run
124
+ */
125
+ export async function runCli({
126
+ argv = process.argv.slice(2),
127
+ cwd = process.cwd(),
128
+ stdout = process.stdout,
129
+ stderr = process.stderr,
130
+ } = {}) {
131
+ let args;
132
+ try {
133
+ args = parseArgs(argv);
134
+ } catch (err) {
135
+ stderr.write(`[knip-entries] ❌ ${err?.message ?? String(err)}\n`);
136
+ return EXIT_CANNOT_RUN;
137
+ }
138
+ const report = await resolveEntrySync({ repoRoot: args.cwd ?? cwd });
139
+
140
+ if (args.json) {
141
+ stdout.write(
142
+ `${JSON.stringify({ kind: 'knip-entry-sync', ...report }, null, 2)}\n`,
143
+ );
144
+ } else if (report.skipped) {
145
+ stdout.write(`[knip-entries] ⏭️ not applicable: ${report.skipped}\n`);
146
+ } else if (report.error) {
147
+ stderr.write(`[knip-entries] ❌ ${report.error}\n`);
148
+ } else {
149
+ stdout.write(`\n--- knip-entries ---\n${renderEntrySyncReport(report)}\n`);
150
+ }
151
+ return exitCodeFor(report);
152
+ }
153
+
154
+ runAsCli(import.meta.url, async () => runCli(), {
155
+ source: 'knip-entries',
156
+ propagateExitCode: true,
157
+ errorPrefix: '[knip-entries] ❌ Fatal error',
158
+ usage: HELP,
159
+ });