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
@@ -72,8 +72,6 @@ const KEY_MEANINGS = Object.freeze({
72
72
  'Root for gitignored scratch output (per-run state, mirrors, logs).',
73
73
  'project.docsContextFiles':
74
74
  'Authoritative files an agent must read before starting any task.',
75
- 'project.commands.lintBaseline':
76
- 'Lint command the close-validation chain runs.',
77
75
  'project.commands.test': 'Test command the close-validation chain runs.',
78
76
  'project.commands.typecheck':
79
77
  'Typecheck command (null disables the typecheck gate).',
@@ -136,8 +134,6 @@ const KEY_MEANINGS = Object.freeze({
136
134
  // delivery.*
137
135
  'delivery.execution.timeoutMs':
138
136
  'Per-execution timeout for orchestrated delivery steps.',
139
- 'delivery.lease.ttlMs':
140
- 'Time-to-live for the delivery lease before a stale claim is reclaimable.',
141
137
  'delivery.ci.watch.pollIntervalMs':
142
138
  'Poll cadence (ms) for the merge/CI watch loop.',
143
139
  'delivery.ci.watch.maxPolls':
@@ -147,7 +143,7 @@ const KEY_MEANINGS = Object.freeze({
147
143
  'delivery.ci.autoMerge':
148
144
  'Merge posture: trust-ci merges on green checks; strict also requires a clean review gate.',
149
145
  'delivery.docsFreshness.paths':
150
- 'Docs whose freshness is checked at delivery time.',
146
+ 'Docs the audit-documentation lens folds into its target set.',
151
147
  'delivery.deliverRunner.concurrencyCap':
152
148
  'Maximum Stories dispatched in parallel within one wave. Default 3 — conservative by design to keep host-quota consumption predictable. Operators running wide waves with adequate parallel-agent quota should raise this to reduce wall-clock time proportionally.',
153
149
  'delivery.worktreeIsolation.enabled':
@@ -202,8 +198,6 @@ const KEY_MEANINGS = Object.freeze({
202
198
  'Whether a dedicated refactor stage runs during delivery.',
203
199
  'delivery.acceptanceEval.maxRounds':
204
200
  'Redraft rounds the per-Story acceptance self-eval loop runs before escalating to agent::blocked (default 2; clamped to a hard ceiling that cannot be disabled).',
205
- 'delivery.acceptanceEval.clusterCeiling':
206
- 'Max acceptance criteria one single-delivery acceptance critic scores per fresh-context pass; the Story ACs are split into ceil(totalACs / clusterCeiling) maker-blind critic clusters (default 4; clamped to [1, 8]).',
207
201
 
208
202
  // qa.*
209
203
  'qa.featureRoot': 'Root directory holding the QA harness .feature files.',
@@ -257,6 +251,10 @@ const PREFIX_MEANINGS = Object.freeze([
257
251
  'QA harness deployment target (baseUrl, per-environment sign-in seam, allowWrites gate).',
258
252
  ],
259
253
  ['qa.personas', 'QA harness persona / credential mapping.'],
254
+ [
255
+ 'qa.gherkinLint',
256
+ 'Static Gherkin corpus gate: per-scope feature/step roots plus the exemption-tag and step-waiver escapes.',
257
+ ],
260
258
  ]);
261
259
 
262
260
  /**
@@ -1,23 +1,49 @@
1
1
  /* node:coverage ignore file -- AJV schema declaration (data-as-code) */
2
2
 
3
- import { GATE_BASE } from './shared.js';
3
+ import { gateBase } from './shared.js';
4
4
 
5
5
  const BUNDLE_DECLARATION = {
6
6
  type: 'object',
7
+ description: 'One built artifact the bundle-size gate weighs.',
7
8
  required: ['name', 'path', 'limit'],
8
9
  properties: {
9
- name: { type: 'string', minLength: 1 },
10
- path: { type: 'string', minLength: 1 },
11
- limit: { type: 'string', minLength: 1 },
10
+ name: {
11
+ type: 'string',
12
+ minLength: 1,
13
+ description: 'Label the rollup row is keyed by.',
14
+ },
15
+ path: {
16
+ type: 'string',
17
+ minLength: 1,
18
+ description: 'Glob or path of the built artifact to measure.',
19
+ },
20
+ limit: {
21
+ type: 'string',
22
+ minLength: 1,
23
+ description: 'Human size ceiling (e.g. `"180 kB"`).',
24
+ },
12
25
  },
13
26
  additionalProperties: false,
14
27
  };
15
28
 
16
29
  export const BUNDLE_SIZE_GATE = {
17
30
  type: 'object',
31
+ description:
32
+ 'Built-artifact size ratchet for web targets. Off by default — it needs declared bundles.',
18
33
  properties: {
19
- ...GATE_BASE,
20
- bundles: { type: 'array', items: BUNDLE_DECLARATION },
34
+ ...gateBase({
35
+ enabled: false,
36
+ baselinePath: 'baselines/bundle-size.json',
37
+ tolerance: { kind: 'percent', value: 0 },
38
+ floors: { '*': {} },
39
+ }),
40
+ bundles: {
41
+ type: 'array',
42
+ items: BUNDLE_DECLARATION,
43
+ description:
44
+ 'The artifacts to weigh. Empty means the gate has nothing to do.',
45
+ default: [],
46
+ },
21
47
  },
22
48
  additionalProperties: false,
23
49
  };
@@ -1,18 +1,38 @@
1
1
  /* node:coverage ignore file -- AJV schema declaration (data-as-code) */
2
2
 
3
- import { GATE_BASE, SAFE_STRING } from './shared.js';
3
+ import { COVERAGE_GATE_DEFAULTS } from '../quality.js';
4
+ import { gateBase, SAFE_STRING } from './shared.js';
4
5
 
5
6
  export const COVERAGE_GATE = {
6
7
  type: 'object',
8
+ description:
9
+ 'Line/branch/function coverage ratchet, read from the Istanbul JSON summary the project test run emits.',
7
10
  properties: {
8
- ...GATE_BASE,
9
- coveragePath: { ...SAFE_STRING, minLength: 1 },
11
+ ...gateBase({
12
+ enabled: COVERAGE_GATE_DEFAULTS.enabled,
13
+ baselinePath: COVERAGE_GATE_DEFAULTS.baselinePath,
14
+ tolerance: COVERAGE_GATE_DEFAULTS.tolerance,
15
+ floors: COVERAGE_GATE_DEFAULTS.floors,
16
+ }),
17
+ coveragePath: {
18
+ ...SAFE_STRING,
19
+ minLength: 1,
20
+ description:
21
+ 'Repo-relative path to the Istanbul `coverage-final.json` the capture step writes and the gate reads.',
22
+ default: COVERAGE_GATE_DEFAULTS.coveragePath,
23
+ },
10
24
  // Story #2136 / Task #2142 — bounded timeout for `npm run test:coverage`.
11
25
  // Wired into `runCapture` via `spawnSync({ timeout, killSignal })`. A
12
26
  // SIGKILL fired by the timeout is translated to exit code 124 (the GNU
13
27
  // `timeout` convention) so close-validation can branch on "hang" vs.
14
- // "test failed". Default 600000 (10 min) resolved in `config/quality.js`.
15
- timeoutMs: { type: 'integer', minimum: 1 },
28
+ // "test failed".
29
+ timeoutMs: {
30
+ type: 'integer',
31
+ minimum: 1,
32
+ description:
33
+ 'Bounded timeout (ms) for the `npm run test:coverage` capture spawn. A SIGKILL at the budget boundary maps to exit 124 so close-validation can tell a hang from a test failure.',
34
+ default: COVERAGE_GATE_DEFAULTS.timeoutMs,
35
+ },
16
36
  },
17
37
  additionalProperties: false,
18
38
  };
@@ -18,9 +18,19 @@
18
18
  */
19
19
  export const INCREMENTAL_COVERAGE_SCHEMA = {
20
20
  type: 'object',
21
+ description:
22
+ 'Story #4981 — opt-in incremental coverage-capture + CRAP-join scoping. Default (key absent) preserves today’s full-repo behaviour byte-for-byte. When `enabled: true`, `coverage-capture.js` scopes `npm run test:coverage` to the files changed against `baseRef` (default: the gate’s own `--ref` / `main`), and the CRAP join treats a method in a file the diff did not touch as resolved by its committed baseline row instead of requiring fresh coverage for it.',
21
23
  properties: {
22
- enabled: { type: 'boolean' },
23
- baseRef: { type: 'string', minLength: 1 },
24
+ enabled: {
25
+ type: 'boolean',
26
+ description: 'Master switch for incremental capture + join scoping.',
27
+ },
28
+ baseRef: {
29
+ type: 'string',
30
+ minLength: 1,
31
+ description:
32
+ 'Git ref the changed-file set is computed against. Omitted falls back to the gate’s own `--ref` (`main`).',
33
+ },
24
34
  },
25
35
  additionalProperties: false,
26
36
  };
@@ -1,41 +1,86 @@
1
1
  /* node:coverage ignore file -- AJV schema declaration (data-as-code) */
2
2
 
3
+ import { CRAP_GATE_DEFAULTS } from '../quality.js';
3
4
  import { INCREMENTAL_COVERAGE_SCHEMA } from './crap-incremental-coverage.schema.js';
4
5
  import {
5
- GATE_BASE,
6
+ gateBase,
6
7
  LIST_OR_EXTENDER_OF_STRINGS,
7
8
  SAFE_STRING,
8
9
  } from './shared.js';
9
10
 
10
11
  export const CRAP_GATE = {
11
12
  type: 'object',
13
+ description:
14
+ 'CRAP (Change Risk Anti-Pattern) ratchet — per-method cyclomatic complexity joined against per-method coverage.',
12
15
  properties: {
13
- ...GATE_BASE,
14
- targetDirs: LIST_OR_EXTENDER_OF_STRINGS,
15
- newMethodCeiling: { type: 'integer', minimum: 1 },
16
- requireCoverage: { type: 'boolean' },
17
- // Story #4775 — fail-closed floor on the per-method coverage JOIN: the
18
- // fraction of methods that must resolve a coverage entry, counted only
19
- // over files that HAVE one, before `update-crap-baseline.js` will
20
- // persist. A broken join is silent by construction (unresolved methods
21
- // are simply absent), so the updater refuses rather than writing a thin
22
- // baseline and logging it as success. Default 0.75.
23
- minMethodResolutionRate: { type: 'number', minimum: 0, maximum: 1 },
16
+ ...gateBase({
17
+ enabled: CRAP_GATE_DEFAULTS.enabled,
18
+ baselinePath: CRAP_GATE_DEFAULTS.baselinePath,
19
+ tolerance: CRAP_GATE_DEFAULTS.tolerance,
20
+ floors: CRAP_GATE_DEFAULTS.floors,
21
+ }),
22
+ targetDirs: {
23
+ ...LIST_OR_EXTENDER_OF_STRINGS,
24
+ description:
25
+ "Directories whose JS sources the CRAP gate scores. Mandrel ships a `src/`-centric default; projects whose executable code lives elsewhere (e.g. this repo's `.agents/scripts/`) override here. The framework default is intentionally not auto-discovered, so an override is the explicit, auditable signal.",
26
+ default: [...CRAP_GATE_DEFAULTS.targetDirs],
27
+ },
28
+ newMethodCeiling: {
29
+ type: 'integer',
30
+ minimum: 1,
31
+ description:
32
+ 'Hard CRAP ceiling applied to a method the diff introduces. A new method above it fails the gate regardless of the baseline.',
33
+ default: CRAP_GATE_DEFAULTS.newMethodCeiling,
34
+ },
35
+ requireCoverage: {
36
+ type: 'boolean',
37
+ description:
38
+ 'When true, the gate refuses to score without a coverage artifact rather than silently reporting complexity-only rows.',
39
+ default: CRAP_GATE_DEFAULTS.requireCoverage,
40
+ },
41
+ // Story #4775 — fail-closed floor on the per-method coverage JOIN.
42
+ minMethodResolutionRate: {
43
+ type: 'number',
44
+ minimum: 0,
45
+ maximum: 1,
46
+ description:
47
+ 'Fail-closed floor on the per-method coverage JOIN (Story #4775): the fraction of methods that must resolve a coverage entry, counted only over files that HAVE one, before `update-crap-baseline.js` will persist. A broken join is silent by construction — unresolved methods are simply absent — so the updater refuses rather than writing a thin baseline and logging it as success. Not enforced below 25 joinable methods, where a diff-scoped run’s rate is noise. Default 0.75; a healthy repo resolves ~98%.',
48
+ },
24
49
  friction: {
25
50
  type: 'object',
26
- properties: { markerKey: { type: 'string', minLength: 1 } },
51
+ description:
52
+ 'Friction-signal wiring for a CRAP baseline regression, so a recurring one can reach the actionable threshold.',
53
+ properties: {
54
+ markerKey: {
55
+ type: 'string',
56
+ minLength: 1,
57
+ description: 'Signal marker key the regression is recorded under.',
58
+ },
59
+ },
27
60
  additionalProperties: false,
61
+ default: { ...CRAP_GATE_DEFAULTS.friction },
62
+ },
63
+ refreshTag: {
64
+ ...SAFE_STRING,
65
+ minLength: 1,
66
+ description:
67
+ 'Commit-subject substring that acknowledges a deliberate CRAP baseline refresh in the compared range. A range commit carrying it that also touches the baseline file demotes head-vs-base regressions; floors stay enforced.',
68
+ default: CRAP_GATE_DEFAULTS.refreshTag,
69
+ },
70
+ refreshTimeoutMs: {
71
+ type: 'integer',
72
+ minimum: 1,
73
+ description:
74
+ 'Bounded timeout (ms) for `npm run crap:update` spawned by the baseline-attribution refresh path. Mirrors `coverage.timeoutMs`: a SIGKILL fired at the budget boundary maps to exit 124 so the close orchestrator can flip the Story to `agent::blocked`. Default 60000 (Story #2165).',
75
+ default: CRAP_GATE_DEFAULTS.refreshTimeoutMs,
76
+ },
77
+ ignoreGlobs: {
78
+ type: 'array',
79
+ items: { type: 'string', minLength: 1 },
80
+ description:
81
+ 'Minimatch glob patterns matched against the canonicalised repo-relative path of each discovered file. Files matching any pattern are excluded from CRAP discovery before scoring. Orthogonal to `components` (grouping) — a file excluded here never appears in any component bucket. Absent or empty preserves the existing IGNORED_DIRS-only behaviour (Story #3217).',
82
+ default: [...CRAP_GATE_DEFAULTS.ignoreGlobs],
28
83
  },
29
- refreshTag: { ...SAFE_STRING, minLength: 1 },
30
- // Story #2165 — bounded timeout for `npm run crap:update` spawned by the
31
- // baseline-attribution refresh path. Mirrors `coverage.timeoutMs` (Story
32
- // #2142): a SIGKILL fired at the budget boundary maps to exit code 124
33
- // so the close orchestrator can flip the Story to `agent::blocked`.
34
- refreshTimeoutMs: { type: 'integer', minimum: 1 },
35
- // Story #3217 — glob patterns matched against canonicalised repo-relative
36
- // paths to exclude files from CRAP discovery before scoring. Orthogonal
37
- // to `components` (grouping). Absent/empty preserves existing behaviour.
38
- ignoreGlobs: { type: 'array', items: { type: 'string', minLength: 1 } },
39
84
  incrementalCoverage: INCREMENTAL_COVERAGE_SCHEMA,
40
85
  },
41
86
  additionalProperties: false,
@@ -1,26 +1,38 @@
1
1
  /* node:coverage ignore file -- AJV schema declaration (data-as-code) */
2
2
 
3
- import { GATE_BASE, LIST_OR_EXTENDER_OF_STRINGS } from './shared.js';
3
+ import { gateBase, LIST_OR_EXTENDER_OF_STRINGS } from './shared.js';
4
4
 
5
- /**
6
- * `delivery.quality.gates.duplication` — code-duplication (DRY) gate
7
- * (Story #3664). Shares the four-field `GATE_BASE` (enabled, baselinePath,
8
- * tolerance, floors, components) and adds the scan-scope extras: the
9
- * `targetDirs` the duplication scanner walks, a bounded refresh timeout
10
- * mirroring crap/MI, and `ignoreGlobs` to exclude files from the scan.
11
- */
12
5
  export const DUPLICATION_GATE = {
13
6
  type: 'object',
7
+ description:
8
+ 'Code-duplication (DRY) gate (Story #3664). Shares the gate base and adds the scan-scope extras: the `targetDirs` the duplication scanner walks, a bounded refresh timeout mirroring crap/MI, and `ignoreGlobs` to exclude files from the scan.',
14
9
  properties: {
15
- ...GATE_BASE,
16
- targetDirs: LIST_OR_EXTENDER_OF_STRINGS,
17
- // Bounded timeout (ms) for the `npm run duplication:update` scanner spawn.
18
- // Mirrors `crap.refreshTimeoutMs` / `maintainability.refreshTimeoutMs`.
19
- refreshTimeoutMs: { type: 'integer', minimum: 1 },
20
- // Glob patterns matched against canonicalised repo-relative paths to
21
- // exclude files from duplication discovery before scoring. Orthogonal to
22
- // `components` (grouping). Absent/empty preserves existing behaviour.
23
- ignoreGlobs: { type: 'array', items: { type: 'string', minLength: 1 } },
10
+ ...gateBase({
11
+ enabled: false,
12
+ baselinePath: 'baselines/duplication.json',
13
+ tolerance: { kind: 'absolute', value: 1 },
14
+ floors: { '*': { percentage: 25 } },
15
+ }),
16
+ targetDirs: {
17
+ ...LIST_OR_EXTENDER_OF_STRINGS,
18
+ description:
19
+ "Directories whose JS sources the duplication (DRY) gate scans for copy-paste clones. Mandrel ships a `src/`-centric default; projects whose executable code lives elsewhere (e.g. this repo's `.agents/scripts/`) override here. The framework default is intentionally not auto-discovered, so an override is the explicit, auditable signal (Story #3664).",
20
+ default: ['src'],
21
+ },
22
+ refreshTimeoutMs: {
23
+ type: 'integer',
24
+ minimum: 1,
25
+ description:
26
+ 'Bounded timeout (ms) for `npm run duplication:update` spawned by the baseline-attribution refresh path. Mirrors `crap.refreshTimeoutMs` / `coverage.timeoutMs`: a SIGKILL fired at the budget boundary maps to exit 124. Default 60000 (Story #3664).',
27
+ default: 60000,
28
+ },
29
+ ignoreGlobs: {
30
+ type: 'array',
31
+ items: { type: 'string', minLength: 1 },
32
+ description:
33
+ 'Minimatch glob patterns matched against the canonicalised repo-relative path of each discovered file. Files matching any pattern are excluded from duplication discovery before scanning. Orthogonal to `components` (grouping). Absent or empty preserves the existing behaviour (Story #3664).',
34
+ default: [],
35
+ },
24
36
  },
25
37
  additionalProperties: false,
26
38
  };
@@ -14,14 +14,17 @@ import { MUTATION_GATE } from './mutation.schema.js';
14
14
  *
15
15
  * Split from the former `lib/config-gates-schema.js` aggregate
16
16
  * (Story #2987) into per-gate sub-schema files under
17
- * `lib/config/gates/`. Shared fragments (`GATE_BASE`, `SAFE_STRING`,
17
+ * `lib/config/gates/`. Shared fragments (`gateBase()`, `SAFE_STRING`,
18
18
  * `TOLERANCE_SCHEMA`, `FLOORS_SCHEMA`, `COMPONENTS_SCHEMA`,
19
19
  * `LIST_OR_EXTENDER_OF_STRINGS`, `NULLABLE_NONEMPTY_SAFE_STRING`) live
20
20
  * in `./shared.js`. Each per-gate file exports one sub-schema literal
21
- * with the gate-specific extras layered on top of `GATE_BASE`.
21
+ * with the gate-specific extras layered on top of `gateBase()`, passing
22
+ * that gate's own base-field defaults (Story #5007).
22
23
  */
23
24
  export const GATES_SCHEMA = {
24
25
  type: 'object',
26
+ description:
27
+ 'The eight quality gates, each sharing the `{ enabled, baselinePath, tolerance, floors, components }` base.',
25
28
  properties: {
26
29
  lint: LINT_GATE,
27
30
  coverage: COVERAGE_GATE,
@@ -1,23 +1,51 @@
1
1
  /* node:coverage ignore file -- AJV schema declaration (data-as-code) */
2
2
 
3
- import { GATE_BASE, NULLABLE_NONEMPTY_SAFE_STRING } from './shared.js';
3
+ import { gateBase, NULLABLE_NONEMPTY_SAFE_STRING } from './shared.js';
4
4
 
5
5
  const LIGHTHOUSE_ROUTE = {
6
6
  type: 'object',
7
+ description: 'One route the Lighthouse run scores.',
7
8
  required: ['path'],
8
9
  properties: {
9
- path: { type: 'string', minLength: 1 },
10
- formFactor: { type: 'string', enum: ['mobile', 'desktop'] },
10
+ path: {
11
+ type: 'string',
12
+ minLength: 1,
13
+ description: 'Route path appended to `baseUrl`.',
14
+ },
15
+ formFactor: {
16
+ type: 'string',
17
+ enum: ['mobile', 'desktop'],
18
+ description: 'Emulated form factor for this route.',
19
+ },
11
20
  },
12
21
  additionalProperties: false,
13
22
  };
14
23
 
15
24
  export const LIGHTHOUSE_GATE = {
16
25
  type: 'object',
26
+ description:
27
+ 'Lighthouse category-score ratchet for web targets. Off by default — it needs a running deployment.',
17
28
  properties: {
18
- ...GATE_BASE,
19
- baseUrl: NULLABLE_NONEMPTY_SAFE_STRING,
20
- routes: { type: 'array', items: LIGHTHOUSE_ROUTE },
29
+ ...gateBase({
30
+ enabled: false,
31
+ baselinePath: 'baselines/lighthouse.json',
32
+ tolerance: { kind: 'absolute', value: 0 },
33
+ floors: {
34
+ '*': { performance: 0, accessibility: 0, bestPractices: 0, seo: 0 },
35
+ },
36
+ }),
37
+ baseUrl: {
38
+ ...NULLABLE_NONEMPTY_SAFE_STRING,
39
+ description:
40
+ 'Origin the routes are resolved against. `null` leaves the gate unusable, which is why it ships disabled.',
41
+ default: null,
42
+ },
43
+ routes: {
44
+ type: 'array',
45
+ items: LIGHTHOUSE_ROUTE,
46
+ description: 'Routes to score on each run.',
47
+ default: [],
48
+ },
21
49
  },
22
50
  additionalProperties: false,
23
51
  };
@@ -1,9 +1,18 @@
1
1
  /* node:coverage ignore file -- AJV schema declaration (data-as-code) */
2
2
 
3
- import { GATE_BASE } from './shared.js';
3
+ import { gateBase } from './shared.js';
4
4
 
5
5
  export const LINT_GATE = {
6
6
  type: 'object',
7
- properties: { ...GATE_BASE },
7
+ description:
8
+ 'Lint-count ratchet. Floors are absolute error/warning counts; the baseline pins the current count so a regression is visible even while the floor is not yet met.',
9
+ properties: {
10
+ ...gateBase({
11
+ enabled: true,
12
+ baselinePath: 'baselines/lint.json',
13
+ tolerance: { kind: 'absolute', value: 0 },
14
+ floors: { '*': { errorCount: 0 } },
15
+ }),
16
+ },
8
17
  additionalProperties: false,
9
18
  };
@@ -1,29 +1,51 @@
1
1
  /* node:coverage ignore file -- AJV schema declaration (data-as-code) */
2
2
 
3
+ import { MAINTAINABILITY_GATE_DEFAULTS } from '../quality.js';
3
4
  import {
4
- GATE_BASE,
5
+ gateBase,
5
6
  LIST_OR_EXTENDER_OF_STRINGS,
6
7
  SAFE_STRING,
7
8
  } from './shared.js';
8
9
 
9
10
  export const MAINTAINABILITY_GATE = {
10
11
  type: 'object',
12
+ description:
13
+ 'Maintainability-index ratchet. Scores per file as the average over its methods, so deleting a small high-MI method can legitimately lower a file’s score.',
11
14
  properties: {
12
- ...GATE_BASE,
13
- targetDirs: LIST_OR_EXTENDER_OF_STRINGS,
15
+ ...gateBase({
16
+ enabled: MAINTAINABILITY_GATE_DEFAULTS.enabled,
17
+ baselinePath: MAINTAINABILITY_GATE_DEFAULTS.baselinePath,
18
+ tolerance: MAINTAINABILITY_GATE_DEFAULTS.tolerance,
19
+ floors: MAINTAINABILITY_GATE_DEFAULTS.floors,
20
+ }),
21
+ targetDirs: {
22
+ ...LIST_OR_EXTENDER_OF_STRINGS,
23
+ description:
24
+ "Directories whose JS sources the maintainability gate scores. Mandrel ships a `src/`-centric default; projects whose executable code lives elsewhere (e.g. this repo's `.agents/scripts/` plus `tests/`) override here. The framework default is intentionally not auto-discovered, so an override is the explicit, auditable signal.",
25
+ default: [...MAINTAINABILITY_GATE_DEFAULTS.targetDirs],
26
+ },
14
27
  // Story #4731 — commit-subject substring that acknowledges a deliberate
15
- // maintainability baseline refresh in the compared range. Mirrors the CRAP
16
- // gate's `refreshTag`; a range commit carrying it that touches the baseline
17
- // file demotes head-vs-base regressions (floors still enforced).
18
- refreshTag: { ...SAFE_STRING, minLength: 1 },
19
- // Story #2165 — bounded timeout for `npm run maintainability:update`
20
- // spawned by the baseline-attribution refresh path. Mirrors
21
- // `coverage.timeoutMs` (Story #2142).
22
- refreshTimeoutMs: { type: 'integer', minimum: 1 },
23
- // Story #3217 — glob patterns matched against canonicalised repo-relative
24
- // paths to exclude files from MI discovery before scoring. Orthogonal to
25
- // `components` (grouping). Absent/empty preserves existing behaviour.
26
- ignoreGlobs: { type: 'array', items: { type: 'string', minLength: 1 } },
28
+ // maintainability baseline refresh in the compared range.
29
+ refreshTag: {
30
+ ...SAFE_STRING,
31
+ minLength: 1,
32
+ description:
33
+ "Commit-subject substring that acknowledges a deliberate maintainability baseline refresh in the compared range. Mirrors the CRAP gate's `refreshTag`; a range commit carrying it that touches the baseline file demotes head-vs-base regressions (floors still enforced).",
34
+ },
35
+ refreshTimeoutMs: {
36
+ type: 'integer',
37
+ minimum: 1,
38
+ description:
39
+ 'Bounded timeout (ms) for `npm run maintainability:update` spawned by the baseline-attribution refresh path. Mirrors `coverage.timeoutMs`: a SIGKILL fired at the budget boundary maps to exit 124 so the close orchestrator can flip the Story to `agent::blocked`. Default 60000 (Story #2165).',
40
+ default: MAINTAINABILITY_GATE_DEFAULTS.refreshTimeoutMs,
41
+ },
42
+ ignoreGlobs: {
43
+ type: 'array',
44
+ items: { type: 'string', minLength: 1 },
45
+ description:
46
+ 'Minimatch glob patterns matched against the canonicalised repo-relative path of each discovered file. Files matching any pattern are excluded from MI discovery before scoring. Orthogonal to `components` (grouping) — a file excluded here never appears in any component bucket. Absent or empty preserves the existing IGNORED_DIRS-only behaviour (Story #3217).',
47
+ default: [...MAINTAINABILITY_GATE_DEFAULTS.ignoreGlobs],
48
+ },
27
49
  },
28
50
  additionalProperties: false,
29
51
  };
@@ -1,12 +1,24 @@
1
1
  /* node:coverage ignore file -- AJV schema declaration (data-as-code) */
2
2
 
3
- import { GATE_BASE, NULLABLE_NONEMPTY_SAFE_STRING } from './shared.js';
3
+ import { gateBase, NULLABLE_NONEMPTY_SAFE_STRING } from './shared.js';
4
4
 
5
5
  export const MUTATION_GATE = {
6
6
  type: 'object',
7
+ description:
8
+ 'Stryker mutation-score ratchet. Off in practice for most consumers — the baseline kind is registered but no framework path runs Stryker.',
7
9
  properties: {
8
- ...GATE_BASE,
9
- strykerConfigPath: NULLABLE_NONEMPTY_SAFE_STRING,
10
+ ...gateBase({
11
+ enabled: true,
12
+ baselinePath: 'baselines/mutation.json',
13
+ tolerance: { kind: 'percent', value: 0 },
14
+ floors: { '*': { score: 60 } },
15
+ }),
16
+ strykerConfigPath: {
17
+ ...NULLABLE_NONEMPTY_SAFE_STRING,
18
+ description:
19
+ 'Repo-relative path to the Stryker config. `null` lets Stryker resolve its own default.',
20
+ default: null,
21
+ },
10
22
  },
11
23
  additionalProperties: false,
12
24
  };
@@ -48,10 +48,21 @@ export const LIST_OR_EXTENDER_OF_STRINGS = {
48
48
  /** Object-shaped tolerance: `{ kind: 'absolute' | 'percent', value: number }`. */
49
49
  export const TOLERANCE_SCHEMA = {
50
50
  type: 'object',
51
+ description:
52
+ 'How much a rollup may drift from the committed baseline before the gate reports a regression.',
51
53
  required: ['kind', 'value'],
52
54
  properties: {
53
- kind: { type: 'string', enum: ['absolute', 'percent'] },
54
- value: { type: 'number', minimum: 0 },
55
+ kind: {
56
+ type: 'string',
57
+ enum: ['absolute', 'percent'],
58
+ description:
59
+ 'Whether `value` is read as raw metric units (`absolute`) or as a percentage of the baseline (`percent`).',
60
+ },
61
+ value: {
62
+ type: 'number',
63
+ minimum: 0,
64
+ description: 'The tolerance magnitude. 0 means no drift is allowed.',
65
+ },
55
66
  },
56
67
  additionalProperties: false,
57
68
  };
@@ -84,6 +95,8 @@ export const TOLERANCE_SCHEMA = {
84
95
  */
85
96
  export const FLOORS_SCHEMA = {
86
97
  type: 'object',
98
+ description:
99
+ 'Workspace-keyed absolute floors: `{ "<workspace>": { "<metric>": number } }`. `"*"` is the project-wide catch-all; the metric keyset is open so per-rollup keys flow through without each gate enumerating them. Floors are absolute — unlike `tolerance`, they are enforced regardless of the baseline.',
87
100
  additionalProperties: {
88
101
  type: 'object',
89
102
  additionalProperties: { type: 'number' },
@@ -102,16 +115,52 @@ export const FLOORS_SCHEMA = {
102
115
  */
103
116
  export const COMPONENTS_SCHEMA = {
104
117
  type: 'object',
118
+ description:
119
+ 'Per-gate component map — component name to the glob list whose files roll up under it. Defaults to `{ "*": ["**"] }` at the resolver layer.',
105
120
  additionalProperties: {
106
121
  type: 'array',
107
122
  items: { type: 'string', minLength: 1 },
108
123
  },
109
124
  };
110
125
 
111
- export const GATE_BASE = {
112
- enabled: { type: 'boolean' },
113
- baselinePath: { ...SAFE_STRING, minLength: 1 },
114
- tolerance: TOLERANCE_SCHEMA,
115
- floors: FLOORS_SCHEMA,
116
- components: COMPONENTS_SCHEMA,
117
- };
126
+ /**
127
+ * Build the shared four-field gate base with per-gate `default` annotations
128
+ * layered on (Story #5007). The base fields are identical in shape across
129
+ * every gate but their defaults are not — each gate ships its own
130
+ * `baselinePath`, tolerance, and floors — so the defaults are supplied by the
131
+ * calling gate module rather than baked in here.
132
+ *
133
+ * @param {{ enabled?: boolean, baselinePath?: string,
134
+ * tolerance?: object, floors?: object }} [defaults]
135
+ * @returns {object} A fresh `properties` fragment; never a shared reference.
136
+ */
137
+ export function gateBase(defaults = {}) {
138
+ return {
139
+ enabled: {
140
+ type: 'boolean',
141
+ description:
142
+ 'When false, the checker exits 0 with a skip line and the gate is reported as `skipped`, never omitted.',
143
+ ...(defaults.enabled === undefined ? {} : { default: defaults.enabled }),
144
+ },
145
+ baselinePath: {
146
+ ...SAFE_STRING,
147
+ minLength: 1,
148
+ description:
149
+ "Repo-root-relative path to the gate's committed baseline artifact.",
150
+ ...(defaults.baselinePath === undefined
151
+ ? {}
152
+ : { default: defaults.baselinePath }),
153
+ },
154
+ tolerance: {
155
+ ...TOLERANCE_SCHEMA,
156
+ ...(defaults.tolerance === undefined
157
+ ? {}
158
+ : { default: defaults.tolerance }),
159
+ },
160
+ floors: {
161
+ ...FLOORS_SCHEMA,
162
+ ...(defaults.floors === undefined ? {} : { default: defaults.floors }),
163
+ },
164
+ components: COMPONENTS_SCHEMA,
165
+ };
166
+ }
@@ -61,7 +61,6 @@ export const NOTIFICATIONS_DEFAULTS = Object.freeze({
61
61
  'operator-message',
62
62
  'merge.unlanded',
63
63
  'merge.flip-failed',
64
- 'loop.tick',
65
64
  ]),
66
65
  });
67
66