mandrel 2.30.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 (253) 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/ci-remediation.md +44 -1
  10. package/.agents/rules/git-conventions-reference.md +27 -27
  11. package/.agents/rules/git-conventions.md +4 -2
  12. package/.agents/rules/known-tooling-behavior.md +66 -30
  13. package/.agents/rules/testing-standards.md +35 -71
  14. package/.agents/runtime-deps.json +0 -1
  15. package/.agents/schemas/agentrc.schema.json +1939 -1400
  16. package/.agents/schemas/lifecycle/README.md +21 -14
  17. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  18. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  19. package/.agents/scripts/README.md +7 -29
  20. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  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 +136 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  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 +0 -1
  38. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  39. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  40. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  41. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  42. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  43. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  44. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  45. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  46. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  47. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  48. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  49. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  50. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  51. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  52. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  53. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  54. package/.agents/scripts/lib/child-exec.js +193 -0
  55. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  56. package/.agents/scripts/lib/cli-args.js +64 -0
  57. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  58. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  59. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  60. package/.agents/scripts/lib/config/commands.js +3 -5
  61. package/.agents/scripts/lib/config/explain.js +5 -7
  62. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  63. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  64. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  65. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  66. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  67. package/.agents/scripts/lib/config/gates/index.js +5 -2
  68. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  69. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  70. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  71. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  72. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  73. package/.agents/scripts/lib/config/github.js +0 -1
  74. package/.agents/scripts/lib/config/limits.js +3 -48
  75. package/.agents/scripts/lib/config/qa.js +105 -0
  76. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  77. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  78. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  79. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  80. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  81. package/.agents/scripts/lib/crap-utils.js +84 -520
  82. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  83. package/.agents/scripts/lib/degraded-mode.js +2 -2
  84. package/.agents/scripts/lib/doc-tiers.js +3 -3
  85. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  86. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  87. package/.agents/scripts/lib/fs-walk.js +52 -0
  88. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  89. package/.agents/scripts/lib/git-utils.js +16 -36
  90. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  91. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  92. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  93. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  94. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  95. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  96. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  97. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  98. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  99. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  100. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  101. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  102. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  103. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  104. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  105. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  106. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  107. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  108. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  109. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  110. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  111. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  112. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  113. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  124. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
  125. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  126. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  127. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  128. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  129. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  130. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  131. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  132. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  133. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  134. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  135. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  136. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  137. package/.agents/scripts/lib/signals/index.js +8 -6
  138. package/.agents/scripts/lib/signals/schema.js +20 -25
  139. package/.agents/scripts/lib/signals/write.js +8 -8
  140. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  141. package/.agents/scripts/lib/temp-retention.js +1 -1
  142. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  143. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  144. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  145. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  146. package/.agents/scripts/lib/worktree-manager.js +2 -3
  147. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  148. package/.agents/scripts/pr-watch-with-update.js +7 -5
  149. package/.agents/scripts/providers/github/cache.js +2 -2
  150. package/.agents/scripts/providers/github/comments.js +6 -28
  151. package/.agents/scripts/providers/github/compose.js +0 -15
  152. package/.agents/scripts/providers/github/errors.js +10 -27
  153. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  154. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  155. package/.agents/scripts/providers/github.js +4 -7
  156. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  157. package/.agents/scripts/resolve-stories.js +0 -2
  158. package/.agents/scripts/run-lint.js +61 -61
  159. package/.agents/scripts/run-test-profile.js +6 -6
  160. package/.agents/scripts/run-verify.js +48 -30
  161. package/.agents/scripts/single-story-close.js +20 -0
  162. package/.agents/scripts/single-story-init.js +12 -35
  163. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  165. package/.agents/skills/skills.index.json +2 -12
  166. package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
  167. package/.agents/workflows/audit-documentation.md +5 -6
  168. package/.agents/workflows/audit-to-stories.md +2 -2
  169. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  170. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  171. package/.agents/workflows/helpers/code-review.md +3 -8
  172. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  173. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  174. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  175. package/.agents/workflows/mandrel-update.md +10 -10
  176. package/.agents/workflows/qa-assist.md +15 -20
  177. package/.agents/workflows/qa-explore.md +9 -8
  178. package/README.md +1 -1
  179. package/docs/CHANGELOG.md +49 -0
  180. package/lib/migrations/index.js +2 -0
  181. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  182. package/package.json +12 -3
  183. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  184. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  185. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  186. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  187. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  188. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  189. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  190. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  191. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  192. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  193. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  194. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  195. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  196. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  197. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  198. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  199. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  200. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  201. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  202. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  203. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  204. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  205. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  206. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  207. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  208. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  209. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  210. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  211. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  212. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  213. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  214. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  215. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  216. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  217. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  218. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  219. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  220. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  221. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  222. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  223. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  224. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  225. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  226. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  227. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  228. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  229. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  230. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  231. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  232. package/.agents/scripts/lib/planning-corpus.js +0 -37
  233. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  234. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  235. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  236. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  237. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  238. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  239. package/.agents/scripts/lib/signals/read.js +0 -268
  240. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  241. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  242. package/.agents/scripts/lib/story-plan.js +0 -379
  243. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  244. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  245. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  246. package/.agents/scripts/lint-baseline.js +0 -507
  247. package/.agents/scripts/providers/github/prs.js +0 -103
  248. package/.agents/scripts/signals-view.js +0 -309
  249. package/.agents/scripts/story-plan.js +0 -370
  250. package/.agents/scripts/sync-branch-from-base.js +0 -149
  251. package/.agents/scripts/validate-docs-freshness.js +0 -314
  252. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  253. package/.agents/workflows/helpers/signals.md +0 -112
@@ -0,0 +1,223 @@
1
+ // .agents/scripts/lib/baselines/scope-assert.js
2
+ //
3
+ // Story #5012 — the pure both-directions row-set assertion, plus the
4
+ // merge-base attribution that decides which half of a divergence is this
5
+ // change set's fault.
6
+ //
7
+ // `check-baselines.js` answers "did a measured value regress?". Nothing
8
+ // answered "does this baseline still describe the tree?" — so a row could
9
+ // point at a file deleted six months ago, or an in-scope file could carry no
10
+ // row at all, and every gate stayed green. Both directions are checked here:
11
+ //
12
+ // - **missing** — an in-scope file with no row. Only asserted for kinds
13
+ // whose producer emits one row per in-scope file
14
+ // (`scope-inventory.js#KIND_SCOPE_POLICY`).
15
+ // - **extra** — a row whose file is gone from disk (`absent`) or no longer
16
+ // matched by the gate's own `targetDirs` / `ignoreGlobs` (`out-of-scope`).
17
+ //
18
+ // ## Why attribution exists
19
+ //
20
+ // Whole-tree equality on the PR path is unusable: the moment anyone lands an
21
+ // in-scope file, every open PR reds on divergence its author did not create
22
+ // and cannot fix from their branch. So the gate blocks only on divergence
23
+ // attributable to `merge-base(base, HEAD)..HEAD`, and reports the inherited
24
+ // remainder as a warning that costs nothing to carry.
25
+ //
26
+ // Attribution can be wrong in one safe direction only, so the resolution
27
+ // **fails towards strict**: no resolvable base, a HEAD not ahead of its base,
28
+ // or a change set that edits a baseline or the config defining its scope all
29
+ // promote every finding to fatal. The last case is the load-bearing one — once
30
+ // a branch has rewritten the scope rules themselves, "which side of the
31
+ // merge-base introduced this row" is no longer a question the diff can answer.
32
+ //
33
+ // Pure module: no filesystem, no git, no config resolution. Every input is
34
+ // handed in already resolved, which is what lets the whole attribution matrix
35
+ // be unit-tested without a fixture repository.
36
+
37
+ /** A row whose keyed file no longer exists on disk. */
38
+ const REASON_ABSENT = 'absent';
39
+ /** A row whose file exists but is no longer inside the gate's scope. */
40
+ const REASON_OUT_OF_SCOPE = 'out-of-scope';
41
+
42
+ export const EXTRA_REASONS = Object.freeze({
43
+ ABSENT: REASON_ABSENT,
44
+ OUT_OF_SCOPE: REASON_OUT_OF_SCOPE,
45
+ });
46
+
47
+ /**
48
+ * Strictness reasons, in resolution order. Exported so the CLI can render the
49
+ * cause without restating the strings.
50
+ */
51
+ export const STRICT_REASONS = Object.freeze({
52
+ NO_BASE: 'no-resolvable-base',
53
+ NOT_AHEAD: 'head-not-ahead-of-base',
54
+ BASELINE_EDITED: 'change-set-edits-a-baseline',
55
+ SCOPE_CONFIG_EDITED: 'change-set-edits-a-scope-config',
56
+ ATTRIBUTABLE: 'attributable-to-merge-base-range',
57
+ });
58
+
59
+ /**
60
+ * Collect the distinct key values a row set carries, skipping malformed rows.
61
+ *
62
+ * @param {Array<Record<string, unknown>>} rows
63
+ * @param {string} keyField
64
+ * @returns {Set<string>}
65
+ */
66
+ function rowKeys(rows, keyField) {
67
+ const keys = new Set();
68
+ for (const row of Array.isArray(rows) ? rows : []) {
69
+ const value = row?.[keyField];
70
+ if (typeof value === 'string' && value.length > 0) keys.add(value);
71
+ }
72
+ return keys;
73
+ }
74
+
75
+ /**
76
+ * Compare a baseline's row set against its in-scope inventory.
77
+ *
78
+ * Direction gating is the inventory's call, not this function's: a kind whose
79
+ * policy omits `missing` produces no missing findings even when its row set is
80
+ * far sparser than the tree, because for that kind sparseness is correct.
81
+ *
82
+ * @param {{
83
+ * inventory: { kind: string, keyField: string, directions: readonly string[],
84
+ * files: string[] | null, degraded: boolean, reason: string | null },
85
+ * rows: Array<Record<string, unknown>>,
86
+ * existsOnDisk?: (relPath: string) => boolean,
87
+ * }} params
88
+ * @returns {{
89
+ * kind: string,
90
+ * skipped: boolean,
91
+ * reason: string | null,
92
+ * missing: string[],
93
+ * extra: Array<{ path: string, reason: string }>,
94
+ * }}
95
+ */
96
+ export function assertScope({ inventory, rows, existsOnDisk } = {}) {
97
+ const kind = inventory?.kind ?? 'unknown';
98
+ const directions = inventory?.directions ?? [];
99
+ if (directions.length === 0 || inventory?.files === null) {
100
+ return {
101
+ kind,
102
+ skipped: true,
103
+ reason: inventory?.reason ?? `${kind} declares no assertable direction`,
104
+ missing: [],
105
+ extra: [],
106
+ };
107
+ }
108
+
109
+ const keyField = inventory.keyField;
110
+ const inScope = new Set(inventory.files);
111
+ const keyed = rowKeys(rows, keyField);
112
+ const onDisk = typeof existsOnDisk === 'function' ? existsOnDisk : () => true;
113
+
114
+ const missing = directions.includes('missing')
115
+ ? inventory.files.filter((file) => !keyed.has(file)).sort()
116
+ : [];
117
+
118
+ const extra = [];
119
+ if (directions.includes('extra')) {
120
+ for (const key of [...keyed].sort()) {
121
+ if (inScope.has(key)) continue;
122
+ extra.push({
123
+ path: key,
124
+ reason: onDisk(key) ? REASON_OUT_OF_SCOPE : REASON_ABSENT,
125
+ });
126
+ }
127
+ }
128
+
129
+ return { kind, skipped: false, reason: null, missing, extra };
130
+ }
131
+
132
+ /**
133
+ * Decide whether this run blocks on every finding or only on the attributable
134
+ * ones. Fails towards strict — each early return below is a case where the
135
+ * merge-base range cannot be trusted to explain the divergence.
136
+ *
137
+ * @param {{
138
+ * base?: string | null,
139
+ * aheadOfBase?: boolean,
140
+ * changedFiles?: string[],
141
+ * baselinePaths?: string[],
142
+ * scopeConfigPaths?: string[],
143
+ * }} params
144
+ * @returns {{ strict: boolean, reason: string }}
145
+ */
146
+ export function resolveStrictness({
147
+ base,
148
+ aheadOfBase,
149
+ changedFiles = [],
150
+ baselinePaths = [],
151
+ scopeConfigPaths = [],
152
+ } = {}) {
153
+ if (typeof base !== 'string' || base.length === 0) {
154
+ return { strict: true, reason: STRICT_REASONS.NO_BASE };
155
+ }
156
+ if (aheadOfBase !== true) {
157
+ return { strict: true, reason: STRICT_REASONS.NOT_AHEAD };
158
+ }
159
+ const changed = new Set(changedFiles);
160
+ if (baselinePaths.some((file) => changed.has(file))) {
161
+ return { strict: true, reason: STRICT_REASONS.BASELINE_EDITED };
162
+ }
163
+ if (scopeConfigPaths.some((file) => changed.has(file))) {
164
+ return { strict: true, reason: STRICT_REASONS.SCOPE_CONFIG_EDITED };
165
+ }
166
+ return { strict: false, reason: STRICT_REASONS.ATTRIBUTABLE };
167
+ }
168
+
169
+ /**
170
+ * Split a kind's findings into the fatal set and the warning set.
171
+ *
172
+ * Under `strict`, everything is fatal. Otherwise a finding is fatal only when
173
+ * this change set created it:
174
+ *
175
+ * - a **missing** row for a file the range ADDED — the branch introduced an
176
+ * in-scope file and left it unmeasured;
177
+ * - an **extra** row for a file the range DELETED or renamed away — the
178
+ * branch removed the file and left its row behind.
179
+ *
180
+ * Everything else is inherited: real, worth reporting, and not this author's
181
+ * to fix from this branch.
182
+ *
183
+ * @param {{
184
+ * missing?: string[],
185
+ * extra?: Array<{ path: string, reason: string }>,
186
+ * added?: string[],
187
+ * removed?: string[],
188
+ * strict?: boolean,
189
+ * }} params
190
+ * @returns {{
191
+ * fatal: { missing: string[], extra: Array<{ path: string, reason: string }> },
192
+ * warning: { missing: string[], extra: Array<{ path: string, reason: string }> },
193
+ * fatalCount: number,
194
+ * warningCount: number,
195
+ * }}
196
+ */
197
+ export function attributeDivergence({
198
+ missing = [],
199
+ extra = [],
200
+ added = [],
201
+ removed = [],
202
+ strict = false,
203
+ } = {}) {
204
+ const addedSet = new Set(added);
205
+ const removedSet = new Set(removed);
206
+ const isFatalMissing = (file) => strict || addedSet.has(file);
207
+ const isFatalExtra = (finding) => strict || removedSet.has(finding.path);
208
+
209
+ const fatal = {
210
+ missing: missing.filter(isFatalMissing),
211
+ extra: extra.filter(isFatalExtra),
212
+ };
213
+ const warning = {
214
+ missing: missing.filter((file) => !isFatalMissing(file)),
215
+ extra: extra.filter((finding) => !isFatalExtra(finding)),
216
+ };
217
+ return {
218
+ fatal,
219
+ warning,
220
+ fatalCount: fatal.missing.length + fatal.extra.length,
221
+ warningCount: warning.missing.length + warning.extra.length,
222
+ };
223
+ }
@@ -0,0 +1,314 @@
1
+ // .agents/scripts/lib/baselines/scope-inventory.js
2
+ //
3
+ // Story #5012 — the ONE in-scope file inventory the honesty surface reads.
4
+ //
5
+ // A ratchet gate can be green while measuring almost nothing: nothing checks
6
+ // that a committed baseline's row set still describes the tree it claims to
7
+ // describe. Both instruments that now do — `check-baseline-scope.js` (assert)
8
+ // and `prune-baseline-orphans.js` (remedy) — resolve "which files is this kind
9
+ // supposed to have rows for?" through this module, so the gate and the pruner
10
+ // cannot disagree about scope and start fighting each other.
11
+ //
12
+ // ## Recompute from the gate's own config, never from a second walker
13
+ //
14
+ // The inventory is derived from **the same configuration the kind's refresh
15
+ // scorer reads**, through the same helpers:
16
+ //
17
+ // - `coverage` — `.c8rc.cjs` `include` / `exclude`, applied with
18
+ // `coverage-baseline.js#buildScopePredicate`, exactly as
19
+ // `refresh-service.js#buildDefaultCoverageScorer` does.
20
+ // - every other file-keyed kind — `delivery.quality.gates.<kind>.targetDirs`
21
+ // / `.ignoreGlobs`, walked with `maintainability-utils.js#scanDirectory`,
22
+ // exactly as `refresh-service.js#buildDefaultMaintainabilityScorer` does.
23
+ //
24
+ // A second walker written here would report the two implementations
25
+ // disagreeing as divergence — the same failure mode
26
+ // `check-baseline-drift.js` avoids by re-scoring through
27
+ // `resolveDefaultScorer` rather than a private scorer.
28
+ //
29
+ // ## Measurement-free by contract
30
+ //
31
+ // Nothing in this module runs a scorer, reads `coverage/coverage-final.json`,
32
+ // or needs a test run to have happened. It walks directories and matches
33
+ // globs. That is what makes the gate runnable on any checkout, which is in
34
+ // turn what makes hard-failing on a stale row defensible.
35
+
36
+ import { createRequire } from 'node:module';
37
+ import path from 'node:path';
38
+
39
+ import { buildScopePredicate } from '../coverage-baseline.js';
40
+ import { scanDirectory } from '../maintainability-utils.js';
41
+
42
+ /**
43
+ * Per-kind classification of which divergence directions may be asserted.
44
+ *
45
+ * **Only a kind whose producer emits one row per in-scope file may assert the
46
+ * MISSING direction.** This is not a conservatism knob — it is arithmetic.
47
+ * Measured against this repository on 2026-08-05, asserting MISSING densely
48
+ * yields 128 phantom rows for `crap` (rows are per-method and coverage-gated,
49
+ * so a file with no scorable method legitimately has none) and 510 for
50
+ * `duplication` (a row exists only where a clone was found). A gate that
51
+ * reports 638 phantom findings on a healthy tree is a gate nobody reads.
52
+ *
53
+ * `lint` and `mutation` are sparse for the same reason — a clean file has no
54
+ * lint row, an untested file has no mutation row. `lighthouse` (keyed on
55
+ * `route`) and `bundle-size` (keyed on `bundle`) are not file-keyed at all, so
56
+ * neither direction is meaningful: their keys name artefacts, not paths on
57
+ * disk, and a directory walk can say nothing about them.
58
+ *
59
+ * @type {Readonly<Record<string, { keyField: string, directions: readonly string[] }>>}
60
+ */
61
+ export const KIND_SCOPE_POLICY = Object.freeze({
62
+ coverage: Object.freeze({
63
+ keyField: 'path',
64
+ directions: Object.freeze(['missing', 'extra']),
65
+ }),
66
+ maintainability: Object.freeze({
67
+ keyField: 'path',
68
+ directions: Object.freeze(['missing', 'extra']),
69
+ }),
70
+ crap: Object.freeze({
71
+ keyField: 'path',
72
+ directions: Object.freeze(['extra']),
73
+ }),
74
+ duplication: Object.freeze({
75
+ keyField: 'path',
76
+ directions: Object.freeze(['extra']),
77
+ }),
78
+ lint: Object.freeze({
79
+ keyField: 'path',
80
+ directions: Object.freeze(['extra']),
81
+ }),
82
+ mutation: Object.freeze({
83
+ keyField: 'path',
84
+ directions: Object.freeze(['extra']),
85
+ }),
86
+ lighthouse: Object.freeze({
87
+ keyField: 'route',
88
+ directions: Object.freeze([]),
89
+ }),
90
+ 'bundle-size': Object.freeze({
91
+ keyField: 'bundle',
92
+ directions: Object.freeze([]),
93
+ }),
94
+ });
95
+
96
+ /** Every kind the honesty surface knows about, in report order. */
97
+ export const SCOPE_KINDS = Object.freeze(Object.keys(KIND_SCOPE_POLICY));
98
+
99
+ /**
100
+ * The directions `kind` may assert. Unknown kinds assert nothing — a kind this
101
+ * module has never heard of has no scope model, and inventing one would be the
102
+ * phantom-row failure above with extra steps.
103
+ *
104
+ * @param {string} kind
105
+ * @returns {readonly string[]}
106
+ */
107
+ export function directionsFor(kind) {
108
+ return KIND_SCOPE_POLICY[kind]?.directions ?? [];
109
+ }
110
+
111
+ /**
112
+ * The row field `kind` keys on — `path` for the file-keyed kinds, `route` for
113
+ * lighthouse, `bundle` for bundle-size. Mirrors `kinds/<kind>.js#keyField`;
114
+ * kept here so a caller can classify without importing the kernel.
115
+ *
116
+ * @param {string} kind
117
+ * @returns {string}
118
+ */
119
+ export function keyFieldFor(kind) {
120
+ return KIND_SCOPE_POLICY[kind]?.keyField ?? 'path';
121
+ }
122
+
123
+ /**
124
+ * True when `kind` keys its rows on a repo-relative file path, so a directory
125
+ * walk can be compared against its row set at all.
126
+ *
127
+ * @param {string} kind
128
+ * @returns {boolean}
129
+ */
130
+ export function isFileKeyed(kind) {
131
+ return keyFieldFor(kind) === 'path' && Object.hasOwn(KIND_SCOPE_POLICY, kind);
132
+ }
133
+
134
+ /** Glob metacharacters that end a pattern's literal prefix. */
135
+ const GLOB_META = /[*?[\]{}!+@()]/;
136
+
137
+ /**
138
+ * Derive directory roots to walk from a list of include globs, by taking each
139
+ * pattern's literal leading path segments.
140
+ *
141
+ * The coverage gate's schema is closed and declares no `targetDirs` — its
142
+ * scope lives entirely in `.c8rc.cjs`. Without this derivation the coverage
143
+ * walk has no root to start from, so there is nothing to compare the row set
144
+ * against and the whole MISSING direction goes silently inert for the one kind
145
+ * whose producer is densest.
146
+ *
147
+ * `'.agents/scripts/**'` yields `.agents/scripts`; a pattern that is glob from
148
+ * its first segment (`'**​/*.js'`) yields `.`, the repo root.
149
+ *
150
+ * @param {string[]} globs
151
+ * @returns {string[]} De-duplicated roots, repo-relative POSIX.
152
+ */
153
+ export function deriveWalkRoots(globs) {
154
+ const roots = new Set();
155
+ for (const glob of Array.isArray(globs) ? globs : []) {
156
+ if (typeof glob !== 'string' || glob.length === 0) continue;
157
+ const literal = [];
158
+ for (const segment of glob.split('/')) {
159
+ if (GLOB_META.test(segment)) break;
160
+ literal.push(segment);
161
+ }
162
+ roots.add(literal.join('/') || '.');
163
+ }
164
+ return [...roots];
165
+ }
166
+
167
+ /**
168
+ * Walk `roots` under `cwd` and return canonical repo-relative POSIX paths.
169
+ *
170
+ * Delegates to `scanDirectory`, which already owns the supported-extension set
171
+ * and the ignored-directory set (`node_modules`, `.git`, `temp`, `.worktrees`,
172
+ * …) that every scorer walks with. Passing `ignoreGlobs` straight through
173
+ * means an ignored file is dropped by the identical matcher the scorer uses.
174
+ *
175
+ * @param {{ cwd: string, roots: string[], ignoreGlobs?: string[] }} params
176
+ * @returns {string[]}
177
+ */
178
+ function walkRoots({ cwd, roots, ignoreGlobs = [] }) {
179
+ const absolute = [];
180
+ for (const root of roots) {
181
+ const abs = path.isAbsolute(root) ? root : path.resolve(cwd, root);
182
+ scanDirectory(abs, absolute, { cwd, ignoreGlobs });
183
+ }
184
+ return absolute.map((abs) =>
185
+ path.relative(cwd, abs).split(path.sep).join('/'),
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Build the coverage inventory from `.c8rc.cjs`.
191
+ *
192
+ * Roots come from the literal prefixes of `include`; the walked set is then
193
+ * filtered through the same `buildScopePredicate` the coverage scorer applies,
194
+ * so `exclude` is honoured by one implementation rather than two.
195
+ *
196
+ * @param {{ cwd: string, requireFn?: (id: string) => object }} params
197
+ * @returns {{ files: string[] | null, roots: string[], degraded: boolean, reason: string | null }}
198
+ */
199
+ function coverageInventory({ cwd, requireFn }) {
200
+ const load = requireFn ?? createRequire(path.join(cwd, 'noop.cjs'));
201
+ let config;
202
+ try {
203
+ config = load(path.resolve(cwd, '.c8rc.cjs'));
204
+ } catch (err) {
205
+ return {
206
+ files: null,
207
+ roots: [],
208
+ degraded: true,
209
+ reason: `.c8rc.cjs unreadable: ${err?.message ?? err}`,
210
+ };
211
+ }
212
+ const include = Array.isArray(config?.include) ? config.include : [];
213
+ const exclude = Array.isArray(config?.exclude) ? config.exclude : [];
214
+ if (include.length === 0) {
215
+ return {
216
+ files: null,
217
+ roots: [],
218
+ degraded: true,
219
+ reason: '.c8rc.cjs declares no include globs',
220
+ };
221
+ }
222
+ const roots = deriveWalkRoots(include);
223
+ const inScope = buildScopePredicate({ include, exclude });
224
+ return {
225
+ files: walkRoots({ cwd, roots }).filter(inScope),
226
+ roots,
227
+ degraded: false,
228
+ reason: null,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Build the inventory for a `targetDirs`-configured kind (everything except
234
+ * coverage). Reads the gate's own `targetDirs` / `ignoreGlobs` off the
235
+ * normalized quality block — the same two keys its refresh scorer reads.
236
+ *
237
+ * @param {{ kind: string, cwd: string, gate: object }} params
238
+ * @returns {{ files: string[] | null, roots: string[], degraded: boolean, reason: string | null }}
239
+ */
240
+ function targetDirsInventory({ kind, cwd, gate }) {
241
+ const roots = Array.isArray(gate?.targetDirs) ? gate.targetDirs : [];
242
+ const ignoreGlobs = Array.isArray(gate?.ignoreGlobs) ? gate.ignoreGlobs : [];
243
+ if (roots.length === 0) {
244
+ return {
245
+ files: null,
246
+ roots: [],
247
+ degraded: true,
248
+ reason: `delivery.quality.gates.${kind}.targetDirs is empty or unset`,
249
+ };
250
+ }
251
+ return {
252
+ files: walkRoots({ cwd, roots, ignoreGlobs }),
253
+ roots,
254
+ degraded: false,
255
+ reason: null,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Resolve the set of files `kind` is configured to have rows for.
261
+ *
262
+ * Never throws: an unreadable or absent scope config degrades to
263
+ * `{ files: null, degraded: true, reason }`. Callers treat a degraded
264
+ * inventory as "scope unknown" — the gate reports it and skips that kind's
265
+ * scope directions, the pruner falls back to orphan-only pruning. Neither may
266
+ * infer emptiness from it: a `files: []` inventory would make every committed
267
+ * row look out-of-scope and hand the pruner a licence to delete the baseline.
268
+ *
269
+ * @param {{
270
+ * kind: string,
271
+ * cwd?: string,
272
+ * quality?: object,
273
+ * requireFn?: (id: string) => object,
274
+ * }} params
275
+ * @returns {{
276
+ * kind: string,
277
+ * keyField: string,
278
+ * directions: readonly string[],
279
+ * files: string[] | null,
280
+ * roots: string[],
281
+ * degraded: boolean,
282
+ * reason: string | null,
283
+ * }}
284
+ */
285
+ export function buildScopeInventory({
286
+ kind,
287
+ cwd = process.cwd(),
288
+ quality,
289
+ requireFn,
290
+ } = {}) {
291
+ const base = {
292
+ kind,
293
+ keyField: keyFieldFor(kind),
294
+ directions: directionsFor(kind),
295
+ };
296
+ if (!isFileKeyed(kind)) {
297
+ return {
298
+ ...base,
299
+ files: null,
300
+ roots: [],
301
+ degraded: false,
302
+ reason: `${kind} is not file-keyed (rows key on "${base.keyField}")`,
303
+ };
304
+ }
305
+ const resolved =
306
+ kind === 'coverage'
307
+ ? coverageInventory({ cwd, requireFn })
308
+ : targetDirsInventory({
309
+ kind,
310
+ cwd,
311
+ gate: quality?.gates?.[kind] ?? {},
312
+ });
313
+ return { ...base, ...resolved };
314
+ }