mandrel 2.25.0 → 2.26.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.
- package/.agents/agents/acceptance-critic.md +10 -6
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +3 -1
- package/.agents/instructions.md +1 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +8 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +89 -6
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +38 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +26 -4
- package/.agents/scripts/lib/baselines/kinds/crap.js +112 -15
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -11
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/crap-engine.js +32 -13
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +13 -0
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +209 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +48 -12
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +29 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +245 -140
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/wave-runner/live-probe.js +24 -14
- package/.agents/scripts/lib/wave-runner/ready-set.js +189 -42
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +4 -10
- package/.agents/scripts/lib/workers/crap-worker.js +2 -10
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +112 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +26 -36
- package/.agents/scripts/single-story-close.js +28 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/stories-wave-tick.js +214 -38
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/deliver.md +13 -4
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +77 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +47 -6
- package/.agents/workflows/helpers/plan-reference.md +15 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +3 -0
- package/README.md +13 -6
- package/docs/CHANGELOG.md +64 -0
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* - `.agentrc.json`
|
|
18
18
|
* - `.claude/`
|
|
19
19
|
* - `node .agents/scripts/`
|
|
20
|
+
* …or (Story #4916) names one of the CLI entry points shipped at the
|
|
21
|
+
* top level of `.agents/scripts/` by **bare basename**, so recognition
|
|
22
|
+
* does not depend on how the caller spelled the reference.
|
|
20
23
|
* - Anything else (or empty input) defaults to `"consumer"`. The default
|
|
21
24
|
* is intentional — most friction comes from consumer code touching
|
|
22
25
|
* framework tooling, and we'd rather under-tag than mis-route a
|
|
@@ -47,6 +50,133 @@ const FRAMEWORK_PREFIXES = Object.freeze([
|
|
|
47
50
|
'node .agents/scripts/',
|
|
48
51
|
]);
|
|
49
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Basenames of the CLI entry points shipped at the top level of
|
|
55
|
+
* `.agents/scripts/` (Story #4916).
|
|
56
|
+
*
|
|
57
|
+
* The prefix scan above recognises a framework script only when the caller
|
|
58
|
+
* spelled a path — `node .agents/scripts/acceptance-eval.js` classifies
|
|
59
|
+
* `framework`, while the same script named bare (`acceptance-eval.js --story
|
|
60
|
+
* 4901`) matched no prefix and fell through to `consumer`, so a real framework
|
|
61
|
+
* defect was routed as consumer-actionable and discarded. Recognition must not
|
|
62
|
+
* depend on how the caller spelled the reference.
|
|
63
|
+
*
|
|
64
|
+
* Kept as **static data**, not a directory read: `classifySignalSource` is
|
|
65
|
+
* called once per signal and MUST stay pure — no filesystem I/O at classify
|
|
66
|
+
* time. The sync test in
|
|
67
|
+
* `tests/lib/observability/source-classifier.test.js` reads the real directory
|
|
68
|
+
* and fails when a script is added, renamed, or removed without this list
|
|
69
|
+
* being updated, so the set cannot go stale and silently restore the blind
|
|
70
|
+
* spot.
|
|
71
|
+
*
|
|
72
|
+
* Scoped to the top level deliberately: those are the entry points a command
|
|
73
|
+
* line names. Library modules under `.agents/scripts/lib/` carry generic
|
|
74
|
+
* basenames (`index.js`, `config.js`) that a consumer command could plausibly
|
|
75
|
+
* name, and widening to them would trade one mis-route for another.
|
|
76
|
+
*
|
|
77
|
+
* @type {readonly string[]}
|
|
78
|
+
*/
|
|
79
|
+
const FRAMEWORK_SCRIPT_BASENAMES = Object.freeze([
|
|
80
|
+
'acceptance-eval.js',
|
|
81
|
+
'agents-bootstrap-github.js',
|
|
82
|
+
'apply-quality-bootstrap.js',
|
|
83
|
+
'audit-baselines.js',
|
|
84
|
+
'audit-labels-bootstrap.js',
|
|
85
|
+
'audit-to-stories.js',
|
|
86
|
+
'boot-sweep.js',
|
|
87
|
+
'bootstrap.js',
|
|
88
|
+
'check-action-pinning.js',
|
|
89
|
+
'check-arch-cycles.js',
|
|
90
|
+
'check-baseline-drift.js',
|
|
91
|
+
'check-baselines.js',
|
|
92
|
+
'check-context-budget.js',
|
|
93
|
+
'check-cyclomatic.js',
|
|
94
|
+
'check-dead-exports.js',
|
|
95
|
+
'check-doc-links.js',
|
|
96
|
+
'check-gherkin-placeholders.js',
|
|
97
|
+
'check-lifecycle-doc-drift.js',
|
|
98
|
+
'check-lifecycle-lint.js',
|
|
99
|
+
'check-schema-references.js',
|
|
100
|
+
'check-test-temp-hygiene.js',
|
|
101
|
+
'check-windows-git-perf.js',
|
|
102
|
+
'check-workflow-citations.js',
|
|
103
|
+
'check-workflow-cli-lint.js',
|
|
104
|
+
'check-workflow-timeouts.js',
|
|
105
|
+
'cleanup-repo-test-temp.js',
|
|
106
|
+
'coverage-capture.js',
|
|
107
|
+
'deliver-light.js',
|
|
108
|
+
'deliver-recover.js',
|
|
109
|
+
'diagnose-friction.js',
|
|
110
|
+
'diagnose.js',
|
|
111
|
+
'drain-pending-cleanup.js',
|
|
112
|
+
'evidence-gate.js',
|
|
113
|
+
'generate-config-docs.js',
|
|
114
|
+
'generate-lens-checklists.js',
|
|
115
|
+
'generate-lifecycle-docs.js',
|
|
116
|
+
'generate-skills-index.js',
|
|
117
|
+
'generate-workflows-doc.js',
|
|
118
|
+
'git-cleanup.js',
|
|
119
|
+
'install-matrix-assert.js',
|
|
120
|
+
'lint-baseline.js',
|
|
121
|
+
'lint-issue-body.js',
|
|
122
|
+
'lint-label-vocabulary.js',
|
|
123
|
+
'mandrel-update-preflight.js',
|
|
124
|
+
'nav-registry-diff.js',
|
|
125
|
+
'notify.js',
|
|
126
|
+
'plan-context.js',
|
|
127
|
+
'plan-critics.js',
|
|
128
|
+
'plan-persist.js',
|
|
129
|
+
'plan-run-epilogue.js',
|
|
130
|
+
'post-structured-comment.js',
|
|
131
|
+
'pr-watch-with-update.js',
|
|
132
|
+
'provision-git-hooks.js',
|
|
133
|
+
'quality-preview.js',
|
|
134
|
+
'quality-watch.js',
|
|
135
|
+
'resolve-doc-tiers.js',
|
|
136
|
+
'resolve-stories.js',
|
|
137
|
+
'resync-status-column.js',
|
|
138
|
+
'run-coverage.js',
|
|
139
|
+
'run-lint.js',
|
|
140
|
+
'run-test-profile.js',
|
|
141
|
+
'run-tests.js',
|
|
142
|
+
'run-verify.js',
|
|
143
|
+
'signals-view.js',
|
|
144
|
+
'single-story-close.js',
|
|
145
|
+
'single-story-confirm-merge.js',
|
|
146
|
+
'single-story-init.js',
|
|
147
|
+
'stories-wave-tick.js',
|
|
148
|
+
'story-plan.js',
|
|
149
|
+
'sync-agentrc.js',
|
|
150
|
+
'sync-branch-from-base.js',
|
|
151
|
+
'sync-claude-agents.js',
|
|
152
|
+
'sync-claude-commands.js',
|
|
153
|
+
'test-isolate.js',
|
|
154
|
+
'test-wrapper.js',
|
|
155
|
+
'update-coverage-baseline.js',
|
|
156
|
+
'update-crap-baseline.js',
|
|
157
|
+
'update-duplication-baseline.js',
|
|
158
|
+
'update-maintainability-baseline.js',
|
|
159
|
+
'update-ticket-state.js',
|
|
160
|
+
'validate-docs-freshness.js',
|
|
161
|
+
'validate-skills.js',
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Membership index over {@link FRAMEWORK_SCRIPT_BASENAMES}, built once at
|
|
166
|
+
* module load so the per-signal scan is a hash lookup.
|
|
167
|
+
*
|
|
168
|
+
* @type {ReadonlySet<string>}
|
|
169
|
+
*/
|
|
170
|
+
const FRAMEWORK_SCRIPT_BASENAME_SET = new Set(FRAMEWORK_SCRIPT_BASENAMES);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Characters a shell-ish command line can wrap a token in. Stripped from both
|
|
174
|
+
* ends before the membership test so `"acceptance-eval.js",` still resolves.
|
|
175
|
+
*
|
|
176
|
+
* @type {RegExp}
|
|
177
|
+
*/
|
|
178
|
+
const TOKEN_TRIM = /^[\s'"`(,;:]+|[\s'"`),;:]+$/g;
|
|
179
|
+
|
|
50
180
|
/**
|
|
51
181
|
* Normalise a value to a string for prefix scanning. Anything that is not
|
|
52
182
|
* a string (undefined, null, numbers, objects) becomes the empty string,
|
|
@@ -78,6 +208,44 @@ function containsFrameworkPrefix(haystack) {
|
|
|
78
208
|
return false;
|
|
79
209
|
}
|
|
80
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Return true when `haystack` names a top-level framework script by **bare
|
|
213
|
+
* basename** — no path segment at all (Story #4916).
|
|
214
|
+
*
|
|
215
|
+
* Whitespace-tokenised and matched whole: only a token that *equals* a known
|
|
216
|
+
* basename counts. A token carrying any path segment is deliberately left to
|
|
217
|
+
* {@link containsFrameworkPrefix} — `./tools/notify.js` is the consumer's own
|
|
218
|
+
* script and must stay `consumer`, while `.agents/scripts/notify.js` already
|
|
219
|
+
* matches a prefix. Likewise `my-notify.js` is not `notify.js`.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} haystack
|
|
222
|
+
* @returns {boolean}
|
|
223
|
+
*/
|
|
224
|
+
function containsFrameworkScriptBasename(haystack) {
|
|
225
|
+
if (haystack.length === 0) return false;
|
|
226
|
+
for (const rawToken of haystack.split(/\s+/)) {
|
|
227
|
+
const token = rawToken.replace(TOKEN_TRIM, '');
|
|
228
|
+
if (FRAMEWORK_SCRIPT_BASENAME_SET.has(token)) return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* True when `haystack` names the framework's own surface, by either
|
|
235
|
+
* recognition route: a path prefix, or a bare framework-script basename.
|
|
236
|
+
*
|
|
237
|
+
* Purely additive over {@link containsFrameworkPrefix} — every string that
|
|
238
|
+
* matched a prefix still matches here, so widening can only ever move a
|
|
239
|
+
* classification from `consumer` to `framework`, never the reverse.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} haystack
|
|
242
|
+
* @returns {boolean}
|
|
243
|
+
*/
|
|
244
|
+
function namesFrameworkSurface(haystack) {
|
|
245
|
+
if (containsFrameworkPrefix(haystack)) return true;
|
|
246
|
+
return containsFrameworkScriptBasename(haystack);
|
|
247
|
+
}
|
|
248
|
+
|
|
81
249
|
/**
|
|
82
250
|
* Classify a friction signal as `"framework"` or `"consumer"`.
|
|
83
251
|
*
|
|
@@ -92,6 +260,10 @@ function containsFrameworkPrefix(haystack) {
|
|
|
92
260
|
* `node .agents/scripts/single-story-init.js` — that's framework friction even
|
|
93
261
|
* though the failing test path lives under the consumer.
|
|
94
262
|
*
|
|
263
|
+
* Either input matches on a framework path prefix **or** on a bare framework
|
|
264
|
+
* script basename (Story #4916), so `single-story-close.js --story 4906` is
|
|
265
|
+
* recognised exactly like its fully-pathed spelling.
|
|
266
|
+
*
|
|
95
267
|
* @param {unknown} failingPath The path of the file or directory the
|
|
96
268
|
* signal blames (e.g. `"tests/foo.test.js"`).
|
|
97
269
|
* @param {unknown} command The command line the signal blames
|
|
@@ -101,8 +273,8 @@ function containsFrameworkPrefix(haystack) {
|
|
|
101
273
|
export function classifyPathSource(failingPath, command) {
|
|
102
274
|
const path = toScanString(failingPath);
|
|
103
275
|
const cmd = toScanString(command);
|
|
104
|
-
if (
|
|
105
|
-
if (
|
|
276
|
+
if (namesFrameworkSurface(path)) return 'framework';
|
|
277
|
+
if (namesFrameworkSurface(cmd)) return 'framework';
|
|
106
278
|
return 'consumer';
|
|
107
279
|
}
|
|
108
280
|
|
|
@@ -229,5 +401,6 @@ export function classifySignalSource(record) {
|
|
|
229
401
|
|
|
230
402
|
export const __testing = Object.freeze({
|
|
231
403
|
FRAMEWORK_PREFIXES,
|
|
404
|
+
FRAMEWORK_SCRIPT_BASENAMES,
|
|
232
405
|
TOOL_DEGRADED_CATEGORY,
|
|
233
406
|
});
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
* @typedef {'fresh'|'inline'} CeremonyMode
|
|
79
79
|
* @typedef {'fresh-critic'|'inline-self-eval'} VerdictOwner
|
|
80
80
|
* @typedef {import('./review-depth.js').ChangeLevel} ChangeLevel
|
|
81
|
-
* @typedef {
|
|
81
|
+
* @typedef {(typeof CEREMONY_PROFILES)[number]} CeremonyProfile
|
|
82
82
|
*/
|
|
83
83
|
|
|
84
84
|
/**
|
|
@@ -93,12 +93,18 @@ export function verdictOwnerForMode(mode) {
|
|
|
93
93
|
return mode === 'fresh' ? 'fresh-critic' : 'inline-self-eval';
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
/**
|
|
97
|
+
* The ceremony-profile vocabulary — the **single** place the three profile
|
|
98
|
+
* names are written. `normalizeCeremonyProfile` is its reader and the
|
|
99
|
+
* `CeremonyProfile` typedef is derived from it, so adding a profile is a
|
|
100
|
+
* one-line change here (Story #4926).
|
|
101
|
+
*
|
|
102
|
+
* @type {readonly ['minimal', 'standard', 'strict']}
|
|
103
|
+
*/
|
|
104
|
+
const CEREMONY_PROFILES = Object.freeze(['minimal', 'standard', 'strict']);
|
|
105
|
+
|
|
106
|
+
/** The profile an absent or unrecognized value degrades to. */
|
|
107
|
+
const DEFAULT_CEREMONY_PROFILE = 'standard';
|
|
102
108
|
|
|
103
109
|
/**
|
|
104
110
|
* Normalize an operator/config ceremony profile. Unknown values degrade to
|
|
@@ -107,11 +113,10 @@ export const CEREMONY_PROFILES = Object.freeze([
|
|
|
107
113
|
* @param {unknown} value
|
|
108
114
|
* @returns {CeremonyProfile}
|
|
109
115
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return 'standard';
|
|
116
|
+
function normalizeCeremonyProfile(value) {
|
|
117
|
+
return CEREMONY_PROFILES.includes(/** @type {CeremonyProfile} */ (value))
|
|
118
|
+
? /** @type {CeremonyProfile} */ (value)
|
|
119
|
+
: DEFAULT_CEREMONY_PROFILE;
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/**
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* @module lib/orchestration/check-baselines/phases/compare
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { EXIT_CONFIG } from '../../../baselines/exit-codes.js';
|
|
12
13
|
import { readBaseFromGit } from '../../../baselines/git-base.js';
|
|
13
14
|
import { getKindModule } from '../../../baselines/kernel.js';
|
|
14
15
|
import { resolveScope } from '../../../baselines/scope.js';
|
|
@@ -30,10 +31,8 @@ export function resolveDispatchScope({ kind, quality, env }) {
|
|
|
30
31
|
kind,
|
|
31
32
|
configScope: cfg.scope,
|
|
32
33
|
configRef: cfg.diffRef,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
envRef: env?.BASELINE_REF,
|
|
36
|
-
},
|
|
34
|
+
envScope: env?.BASELINE_SCOPE,
|
|
35
|
+
envRef: env?.BASELINE_REF,
|
|
37
36
|
});
|
|
38
37
|
}
|
|
39
38
|
|
|
@@ -41,13 +40,44 @@ function emptyCompareResult(baseRef) {
|
|
|
41
40
|
return { baseRef, baseRead: false };
|
|
42
41
|
}
|
|
43
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Story #4914 — a base read that FAILS is not a base that is ABSENT.
|
|
45
|
+
*
|
|
46
|
+
* `readBaseFromGit` already draws that line itself: it returns `null` only
|
|
47
|
+
* for git exit 128 ("path does not exist in this revision") and throws on
|
|
48
|
+
* everything else. Swallowing the throw conflated the two, so a broken read
|
|
49
|
+
* silently emptied the whole head-vs-base arm — regressions AND additions —
|
|
50
|
+
* while the floors arm kept the run at exit 0. A gate that fails open is
|
|
51
|
+
* worse than no gate, because it is trusted.
|
|
52
|
+
*
|
|
53
|
+
* So the read failure fails CLOSED as `EXIT_CONFIG` (3) — "the gate could
|
|
54
|
+
* not even start", the same code `assertFloorAxesExist` uses for a
|
|
55
|
+
* misconfigured floor axis. `check-baselines.js#main` maps any throw out of
|
|
56
|
+
* the pipeline onto that code.
|
|
57
|
+
*/
|
|
58
|
+
function buildBaseReadError({ kind, ref, file, cause }) {
|
|
59
|
+
const detail = cause?.message ?? String(cause);
|
|
60
|
+
const err = new Error(
|
|
61
|
+
`[check-baselines:${kind}] could not read the base baseline at ` +
|
|
62
|
+
`${ref}:${file} — the head-vs-base compare arm cannot run, so the gate ` +
|
|
63
|
+
`fails closed rather than reporting zero regressions: ${detail}`,
|
|
64
|
+
);
|
|
65
|
+
err.code = 'EXIT_CONFIG';
|
|
66
|
+
err.exitCode = EXIT_CONFIG;
|
|
67
|
+
err.kind = kind;
|
|
68
|
+
err.baseRef = ref;
|
|
69
|
+
err.baselinePath = file;
|
|
70
|
+
err.cause = cause;
|
|
71
|
+
return err;
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
function readBaseBaselinePayload(scope, kind, gateBlock, cwd) {
|
|
45
75
|
const rel = baselineRelativePath(kind, gateBlock);
|
|
46
76
|
let raw;
|
|
47
77
|
try {
|
|
48
78
|
raw = readBaseFromGit(scope.ref, rel, { cwd });
|
|
49
|
-
} catch {
|
|
50
|
-
|
|
79
|
+
} catch (cause) {
|
|
80
|
+
throw buildBaseReadError({ kind, ref: scope.ref, file: rel, cause });
|
|
51
81
|
}
|
|
52
82
|
if (raw === null) return null;
|
|
53
83
|
try {
|
|
@@ -206,6 +206,11 @@ function buildGateReport({
|
|
|
206
206
|
additions: compareOutput.additions ?? [],
|
|
207
207
|
regressionCount: compareOutput.regressions.length,
|
|
208
208
|
baseRef: cmp.baseRef ?? null,
|
|
209
|
+
// Story #4914 — the compare arm's read status was internal to compare.js,
|
|
210
|
+
// which is why a dead compare arm looked byte-identical to a clean run.
|
|
211
|
+
// Surfacing it makes "the head-vs-base arm did not run" diagnosable from
|
|
212
|
+
// the JSON report alone.
|
|
213
|
+
baseRead: cmp.baseRead === true,
|
|
209
214
|
generatedAt: baseline.generatedAt,
|
|
210
215
|
acknowledged,
|
|
211
216
|
};
|
|
@@ -10,7 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
import { EXIT_CONFIG } from '../../../baselines/exit-codes.js';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Which way a floor axis is compared: `gte` means the measured value must be
|
|
15
|
+
* at or above the floor, `lte` at or below it. Exported (Story #4902) so the
|
|
16
|
+
* baseline hotspot engine reports floor headroom with the same polarity the
|
|
17
|
+
* gate enforces — a second copy of this table would let the two disagree
|
|
18
|
+
* about which direction is "better" for a given axis.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} kind
|
|
21
|
+
* @param {string} axis
|
|
22
|
+
* @returns {'gte' | 'lte'}
|
|
23
|
+
*/
|
|
24
|
+
export function axisDirection(kind, axis) {
|
|
14
25
|
if (kind === 'lint') return 'lte';
|
|
15
26
|
if (kind === 'crap') return 'lte';
|
|
16
27
|
if (kind === 'bundle-size') return 'lte';
|
|
@@ -27,8 +27,15 @@ function formatGateLine(g) {
|
|
|
27
27
|
? ''
|
|
28
28
|
: ` [kernel drift ${g.kernelBaseline} → ${g.kernelCurrent}]`;
|
|
29
29
|
const baseRef = g.baseRef ? ` [baseRef=${g.baseRef}]` : '';
|
|
30
|
+
// Story #4914 — a compare arm that never read its base reports zero
|
|
31
|
+
// regressions and zero additions, which is indistinguishable from a clean
|
|
32
|
+
// run unless the text report says so out loud.
|
|
33
|
+
const baseRead =
|
|
34
|
+
g.baseRef && g.baseRead === false
|
|
35
|
+
? ' [baseRead=false — compare skipped]'
|
|
36
|
+
: '';
|
|
30
37
|
const ack = g.acknowledged ? ' [ACKNOWLEDGED — this run only]' : '';
|
|
31
|
-
return ` - ${g.kind}: ${status}${drift}${baseRef}${ack}`;
|
|
38
|
+
return ` - ${g.kind}: ${status}${drift}${baseRef}${baseRead}${ack}`;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
function formatViolationLine(component, v) {
|
|
@@ -26,8 +26,8 @@ import { buildGlobFilter } from './filters.js';
|
|
|
26
26
|
import { promptStashDecision, promptYesNo } from './prompts.js';
|
|
27
27
|
import { executePrune } from './prune.js';
|
|
28
28
|
import {
|
|
29
|
+
renderCandidateList,
|
|
29
30
|
renderDeferredLine,
|
|
30
|
-
renderDryRun,
|
|
31
31
|
renderExecutionLine,
|
|
32
32
|
renderExecutionSummary,
|
|
33
33
|
renderPruneLine,
|
|
@@ -41,8 +41,9 @@ import {
|
|
|
41
41
|
const TAG = '[git-cleanup]';
|
|
42
42
|
|
|
43
43
|
/* node:coverage ignore next */
|
|
44
|
-
function
|
|
45
|
-
for (const
|
|
44
|
+
function emitCandidateList(plan, opts, baseBranch) {
|
|
45
|
+
for (const l of renderCandidateList({ plan, opts, baseBranch }))
|
|
46
|
+
Logger.info(l);
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/* node:coverage ignore next */
|
|
@@ -291,7 +292,7 @@ export async function runBranchPhase(opts, cwd, baseBranch) {
|
|
|
291
292
|
filter,
|
|
292
293
|
includeRemoteOnly: true,
|
|
293
294
|
});
|
|
294
|
-
|
|
295
|
+
emitCandidateList(plan, opts, baseBranch);
|
|
295
296
|
const action = decideBranchPhase({ plan, opts, cwd });
|
|
296
297
|
if (action.kind === 'prompt-then-execute') {
|
|
297
298
|
const go = await promptYesNo(action.promptMessage);
|
|
@@ -375,7 +376,11 @@ export async function executeStashPhase(action) {
|
|
|
375
376
|
);
|
|
376
377
|
}
|
|
377
378
|
|
|
378
|
-
|
|
379
|
+
// Story #4922 — the `node:coverage ignore next` directive that used to sit
|
|
380
|
+
// here is gone. It was never justified: `runStashPhase` is drivable end to
|
|
381
|
+
// end (planStashes degrades to an empty list outside a repo, and the
|
|
382
|
+
// decide/execute pair below is pure given an allowlist), so the directive
|
|
383
|
+
// only hid a sequencer nothing exercised.
|
|
379
384
|
export async function runStashPhase(opts, cwd) {
|
|
380
385
|
Logger.info(`${TAG} ── phase: stashes ──`);
|
|
381
386
|
const { stashes } = planStashes({ cwd });
|
|
@@ -61,11 +61,28 @@ function contentMergedNote(candidate) {
|
|
|
61
61
|
: '';
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Pure: render the branch-phase candidate list as the operator-facing text
|
|
66
|
+
* block.
|
|
67
|
+
*
|
|
68
|
+
* The header states the run mode: `execute` opts into the reap wording and
|
|
69
|
+
* defaults to `false`, so a caller that forgets it still gets the harmless
|
|
70
|
+
* preview line. Prefer {@link renderCandidateList}, which derives the mode
|
|
71
|
+
* from the phase's own CLI options rather than making the caller restate
|
|
72
|
+
* it — the header used to be hardcoded to the preview wording, so an
|
|
73
|
+
* `--execute` run announced "nothing deleted" and then reaped.
|
|
74
|
+
*
|
|
75
|
+
* @param {{ candidates: Array, skipped?: Array, ghDegraded?: boolean }} plan
|
|
76
|
+
* @param {{ baseBranch?: string|null, now?: number, execute?: boolean }} [opts]
|
|
77
|
+
* @returns {string[]}
|
|
78
|
+
*/
|
|
65
79
|
export function renderDryRun(plan, opts = {}) {
|
|
66
|
-
const { baseBranch = null, now } = opts;
|
|
80
|
+
const { baseBranch = null, now, execute = false } = opts;
|
|
81
|
+
const count = plan.candidates.length;
|
|
67
82
|
const lines = [
|
|
68
|
-
|
|
83
|
+
execute
|
|
84
|
+
? `${TAG} EXECUTE — ${count} candidate(s) to reap`
|
|
85
|
+
: `${TAG} DRY RUN (nothing deleted) — ${count} candidate(s)`,
|
|
69
86
|
];
|
|
70
87
|
if (plan.candidates.length === 0) {
|
|
71
88
|
lines.push(' (no merged branches to clean up)');
|
|
@@ -105,6 +122,25 @@ export function renderDryRun(plan, opts = {}) {
|
|
|
105
122
|
return lines;
|
|
106
123
|
}
|
|
107
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Pure: the branch phase's candidate-list block, with the header's run
|
|
127
|
+
* mode derived from the phase's own CLI options — the same `opts.dryRun`
|
|
128
|
+
* the reap path reads. Taking the whole bag (rather than a restated
|
|
129
|
+
* boolean) is the point: the driver cannot get the wording wrong because
|
|
130
|
+
* it never names the flag, so a destructive `--execute` run can no longer
|
|
131
|
+
* announce itself as a `DRY RUN (nothing deleted)` preview and then
|
|
132
|
+
* delete every candidate.
|
|
133
|
+
*
|
|
134
|
+
* @param {object} args
|
|
135
|
+
* @param {{ candidates: Array, skipped?: Array }} args.plan
|
|
136
|
+
* @param {{ dryRun?: boolean }} args.opts Parsed CLI options.
|
|
137
|
+
* @param {string|null} [args.baseBranch]
|
|
138
|
+
* @returns {string[]}
|
|
139
|
+
*/
|
|
140
|
+
export function renderCandidateList({ plan, opts = {}, baseBranch = null }) {
|
|
141
|
+
return renderDryRun(plan, { baseBranch, execute: !opts.dryRun });
|
|
142
|
+
}
|
|
143
|
+
|
|
108
144
|
/**
|
|
109
145
|
* Pure: render a single latest-PR-state skip line. Returns null when the
|
|
110
146
|
* skip reason is not one of the latest-PR family — `renderDryRun` filters
|