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,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`
@@ -1,14 +1,13 @@
1
1
  /**
2
- * bootstrap/manifest — single mutation-manifest source for the
3
- * consent-first phased install (Story #3521, Feature #3515, Epic #3438).
2
+ * bootstrap/manifest — single mutation-manifest source for the install
3
+ * ledger (Story #3521, Feature #3515, Epic #3438).
4
4
  *
5
5
  * `buildMutationManifest(ctx)` enumerates every mutation the bootstrap
6
- * pipeline can perform as a flat, declarative array. Each entry carries the
7
- * five fields the consent-first install screen needs to preview a change
8
- * before any write lands:
6
+ * pipeline can perform as a flat, declarative array. Each entry carries five
7
+ * fields:
9
8
  *
10
- * - `phaseGroup` — one of the four independently-approvable groups
11
- * (`ide-wiring`, `repo-config`, `github-admin`, `quality-gates`).
9
+ * - `phaseGroup` — one of the four groups (`ide-wiring`, `repo-config`,
10
+ * `github-admin`, `quality-gates`).
12
11
  * - `target` — the file path or remote resource the mutation touches.
13
12
  * - `action` — the verb describing the mutation (`create`, `merge`,
14
13
  * `update`, `run`, `configure`).
@@ -17,16 +16,18 @@
17
16
  * (e.g. delete a created file) vs. a remote-admin mutation that is not
18
17
  * a simple local revert.
19
18
  *
20
- * The manifest is the SINGLE source of truth: `applyProjectBootstrap`'s
21
- * no-write preview is derived from `buildMutationManifest` so the preview
22
- * the operator approves and the execution that follows enumerate the exact
23
- * same set of mutations. There is no second, drift-prone list.
19
+ * Story #3690 replaced the consent-first phased-approval install screen with
20
+ * a plain summary+confirm loop, and Story #5007 removed the vestigial
21
+ * machinery it left behind (the no-write `previewMutationManifest` renderer
22
+ * and the `approvedGroups` set-threading). What survives is load-bearing:
23
+ * `recordLedger` filters these entries into the install ledger, and
24
+ * `mandrel uninstall` walks them — the `phaseGroup` string is the ledger's
25
+ * grouping key, not an approval gate.
24
26
  *
25
27
  * The manifest describes *intended* mutations deterministically from the
26
28
  * project root; it does not read or mutate any file. Whether a given
27
29
  * mutation is a no-op on a particular clone (the file already carries the
28
- * wiring) is decided at execution time by the idempotent `ensure*` steps
29
- * the manifest always lists the full surface so the preview is complete.
30
+ * wiring) is decided at execution time by the idempotent `ensure*` steps.
30
31
  *
31
32
  * @module bootstrap/manifest
32
33
  */
@@ -34,9 +35,9 @@
34
35
  import path from 'node:path';
35
36
 
36
37
  /**
37
- * The four independently-approvable phase groups. The consent-first install
38
- * flow gates each group behind its own opt-in, so every manifest entry MUST
39
- * carry exactly one of these values.
38
+ * The four phase groups. Ledger/uninstall metadata only (Story #5007): every
39
+ * manifest entry MUST carry exactly one of these values so the install ledger
40
+ * can group and the uninstaller can select what a run actually landed.
40
41
  *
41
42
  * @type {Readonly<{ IDE_WIRING: 'ide-wiring', REPO_CONFIG: 'repo-config',
42
43
  * GITHUB_ADMIN: 'github-admin', QUALITY_GATES: 'quality-gates' }>}
@@ -271,27 +272,3 @@ export function buildMutationManifest(ctx = {}) {
271
272
 
272
273
  return entries;
273
274
  }
274
-
275
- /**
276
- * Render the manifest as a no-write preview report grouped by phase group.
277
- * Pure helper — derives entirely from {@link buildMutationManifest}, so the
278
- * preview and the executing pipeline enumerate one identical source.
279
- *
280
- * The returned shape is `{ preview: true, groups: { <phaseGroup>: entry[] },
281
- * entries: entry[] }`: callers that want the flat list read `entries`, and
282
- * the consent-first screen reads `groups` to render one approvable section
283
- * per phase group (only groups with at least one entry appear).
284
- *
285
- * @param {object} [ctx] — same context as {@link buildMutationManifest}.
286
- * @returns {{ preview: true, groups: Record<string, MutationManifestEntry[]>,
287
- * entries: MutationManifestEntry[] }}
288
- */
289
- export function previewMutationManifest(ctx = {}) {
290
- const entries = buildMutationManifest(ctx);
291
- const groups = {};
292
- for (const entry of entries) {
293
- if (!groups[entry.phaseGroup]) groups[entry.phaseGroup] = [];
294
- groups[entry.phaseGroup].push(entry);
295
- }
296
- return { preview: true, groups, entries };
297
- }
@@ -20,7 +20,7 @@ import { isCommandExcluded } from '../command-header.js';
20
20
  import { detectPackageManager as detectPm } from '../detect-package-manager.js';
21
21
  import { LEDGER_RELATIVE_PATH } from './install-ledger.js';
22
22
  import { ensureIssueForms } from './issue-forms-template.js';
23
- import { PHASE_GROUPS, previewMutationManifest } from './manifest.js';
23
+ import { PHASE_GROUPS } from './manifest.js';
24
24
  import { applyQualityBootstrap } from './quality-bootstrap.js';
25
25
 
26
26
  export const SYNC_COMMAND = 'node .agents/scripts/sync-claude-commands.js';
@@ -436,8 +436,7 @@ export function ensureGitignore(ctx) {
436
436
  * project (Story #4227). Derived from the Story-body SSOT so a human-filed
437
437
  * ticket round-trips through `story-body.parse()`. Idempotent and additive,
438
438
  * mirroring `ensureGitignore`: byte-identical forms are `unchanged`,
439
- * operator-edited forms are preserved (`custom-skip`). Honours `ctx.preview`
440
- * (no writes) like the other phases.
439
+ * operator-edited forms are preserved (`custom-skip`).
441
440
  *
442
441
  * Returns the per-form action envelope keyed by ticket type.
443
442
  *
@@ -446,13 +445,9 @@ export function ensureGitignore(ctx) {
446
445
  *
447
446
  * @param {object} ctx
448
447
  * @param {string} ctx.projectRoot
449
- * @param {boolean} [ctx.preview]
450
448
  */
451
449
  function ensureIssueFormsPhase(ctx) {
452
- const { forms } = ensureIssueForms({
453
- projectRoot: ctx.projectRoot,
454
- write: !ctx.preview,
455
- });
450
+ const { forms } = ensureIssueForms({ projectRoot: ctx.projectRoot });
456
451
  const outcomes = {};
457
452
  for (const form of forms) {
458
453
  outcomes[form.type] = { action: form.action, path: form.path };
@@ -691,12 +686,11 @@ const fatalParity = (result) =>
691
686
  * bootstrap.
692
687
  *
693
688
  * Each project-side mutation phase carries a `phaseGroup` matching one of
694
- * the consent-first {@link PHASE_GROUPS}. When a phased-approval gate is
695
- * supplied via `ctx.approvedGroups`, a phase whose `phaseGroup` is not in
696
- * the approved set is skipped (recorded as a `phase-group-declined` no-op)
697
- * declining one group never short-circuits the others (Story #3524).
698
- * Phases with no `phaseGroup` (the Node-version precondition and the
699
- * dependency install) are always-run infrastructure, never gated.
689
+ * the {@link PHASE_GROUPS}. Since Story #5007 that string is pure
690
+ * ledger/uninstall metadata every phase runs on every install, and the
691
+ * ledger decides after the fact which groups actually landed. Phases with no
692
+ * `phaseGroup` (the Node-version precondition and the dependency install)
693
+ * are always-run infrastructure.
700
694
  */
701
695
  export const BOOTSTRAP_PHASES = Object.freeze([
702
696
  {
@@ -770,24 +764,6 @@ export const BOOTSTRAP_PHASES = Object.freeze([
770
764
  },
771
765
  ]);
772
766
 
773
- /**
774
- * Decide whether a phase should run given the approved-phase-group gate.
775
- * An always-run infrastructure phase (no `phaseGroup`) runs unconditionally.
776
- * A grouped phase runs only when no gate is supplied (`approvedGroups`
777
- * absent — the un-gated legacy path) or when its group is in the gate.
778
- *
779
- * Exported for unit testing.
780
- *
781
- * @param {BootstrapPhase} phase
782
- * @param {Set<string>|undefined} approvedGroups
783
- * @returns {boolean}
784
- */
785
- export function isPhaseApproved(phase, approvedGroups) {
786
- if (!phase.phaseGroup) return true;
787
- if (!approvedGroups) return true;
788
- return approvedGroups.has(phase.phaseGroup);
789
- }
790
-
791
767
  /**
792
768
  * Throw with the formatted message when the phase is marked fatal and
793
769
  * the result indicates an abort. Pure helper so the driver stays a
@@ -809,12 +785,6 @@ export function throwIfFatal(phase, result) {
809
785
  * the result on `report[phase.name]`, then route fatal phases through
810
786
  * `throwIfFatal`. Returns the accumulated report.
811
787
  *
812
- * When `ctx.approvedGroups` is a `Set`, a grouped phase whose `phaseGroup`
813
- * is not approved is skipped and recorded as
814
- * `{ skipped: true, reason: 'phase-group-declined', phaseGroup }` — it never
815
- * runs, never throws (so a declined `ide-wiring` group also skips its
816
- * fatal `parity` check), and never short-circuits the remaining phases.
817
- *
818
788
  * Exported for tests so phase ordering and fatal behaviour can be
819
789
  * asserted without spawning a full bootstrap.
820
790
  *
@@ -825,14 +795,6 @@ export function throwIfFatal(phase, result) {
825
795
  export async function runPhases(phases, ctx) {
826
796
  const report = {};
827
797
  for (const phase of phases) {
828
- if (!isPhaseApproved(phase, ctx.approvedGroups)) {
829
- report[phase.name] = {
830
- skipped: true,
831
- reason: 'phase-group-declined',
832
- phaseGroup: phase.phaseGroup,
833
- };
834
- continue;
835
- }
836
798
  const result = await phase.run(ctx, report);
837
799
  report[phase.name] = result;
838
800
  throwIfFatal(phase, result);
@@ -844,24 +806,16 @@ export async function runPhases(phases, ctx) {
844
806
  * Compose every step in order. Each returned key is the outcome of one
845
807
  * step so the CLI can render a structured summary.
846
808
  *
847
- * When `ctx.preview` is truthy, the function performs **no writes and no
848
- * network I/O**. Instead it derives the operator-facing change list from
849
- * the single mutation-manifest source ({@link previewMutationManifest}) and
850
- * returns `{ preview: true, groups, entries }` the exact same source the
851
- * consent-first install screen renders. Deriving the preview from
852
- * `buildMutationManifest` (rather than from a parallel hand-maintained list)
853
- * guarantees the preview the operator approves and the execution that
854
- * follows enumerate one identical set of mutations (Story #3521).
809
+ * Story #5007 removed the no-write `ctx.preview` branch: it had zero
810
+ * production callers (bootstrap's own `--dry-run` renders a hand-rolled text
811
+ * plan instead) and it survived only to serve the consent-first install
812
+ * screen Story #3690 replaced with a plain summary+confirm loop.
855
813
  *
856
814
  * @param {object} ctx
857
815
  * @param {string} ctx.projectRoot
858
816
  * @param {string} [ctx.agentRoot]
859
817
  * @param {{ owner: string, repo: string, baseBranch: string,
860
818
  * operatorHandle: string|null }} ctx.answers
861
- * @param {boolean} [ctx.preview] — no-write preview from the manifest.
862
- * @param {Set<string>} [ctx.approvedGroups] — when present, only phases
863
- * whose `phaseGroup` is in this set execute (the consent-first gate from
864
- * Story #3524); always-run infrastructure phases ignore it.
865
819
  * @param {boolean} [ctx.withQuality]
866
820
  * @param {boolean} [ctx.skipGithub]
867
821
  * @param {boolean} [ctx.skipInstall]
@@ -871,6 +825,5 @@ export async function runPhases(phases, ctx) {
871
825
  * @returns {Promise<object>}
872
826
  */
873
827
  export async function applyProjectBootstrap(ctx) {
874
- if (ctx.preview) return previewMutationManifest(ctx);
875
828
  return runPhases(BOOTSTRAP_PHASES, ctx);
876
829
  }