mandrel 2.18.0 → 2.20.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/docs/SDLC.md +1 -1
- package/.agents/docs/agentrc-reference.json +0 -21
- package/.agents/docs/configuration.md +11 -14
- package/.agents/docs/execution-reference.md +8 -5
- package/.agents/schemas/agentrc.schema.json +0 -31
- package/.agents/scripts/check-doc-links.js +141 -9
- package/.agents/scripts/check-test-temp-hygiene.js +153 -14
- package/.agents/scripts/lib/baselines/env-overrides.js +40 -48
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +3 -4
- package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -2
- package/.agents/scripts/lib/config/explain.js +0 -8
- package/.agents/scripts/lib/config/temp-paths.js +12 -1
- package/.agents/scripts/lib/config-settings-schema.js +8 -24
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +51 -77
- package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +20 -12
- package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +2 -1
- package/.agents/scripts/lib/orchestration/plan-context.js +13 -14
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +12 -66
- package/.agents/scripts/lib/test-env.js +15 -3
- package/.agents/scripts/lib/test-temp.js +311 -0
- package/.agents/workflows/audit-performance.md +2 -2
- package/.agents/workflows/helpers/diagnose.md +1 -1
- package/.agents/workflows/helpers/plan-reference.md +19 -4
- package/.agents/workflows/helpers/signals.md +2 -2
- package/.agents/workflows/mandrel-update.md +4 -4
- package/.agents/workflows/plan.md +7 -6
- package/docs/CHANGELOG.md +16 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +113 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/codebase-snapshot.js +0 -513
- package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +0 -147
- package/.agents/scripts/lib/orchestration/spec-freshness.js +0 -129
|
@@ -83,69 +83,61 @@ export function resolveCrapEnvOverrides(crapConfig, env) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* (mirroring `CRAP_TOLERANCE`'s env-override precedent) tells
|
|
93
|
-
* `check-baselines --gate bundle-size` to treat this run's head
|
|
94
|
-
* measurements as the newly acknowledged baseline: head-vs-base
|
|
95
|
-
* regressions are demoted to `unchanged` for this invocation only. Floors
|
|
96
|
-
* still apply — an acknowledged PR can still fail on an absolute budget
|
|
97
|
-
* breach, only the ratchet-vs-`origin/main` comparison is suspended.
|
|
86
|
+
* The env var that acknowledges a deliberate baseline refresh for `kind`.
|
|
87
|
+
* Upper-snakes the kind name, so `bundle-size` → `BUNDLE_SIZE_REFRESH` and
|
|
88
|
+
* `coverage` → `COVERAGE_REFRESH`. The two names that predate the generic
|
|
89
|
+
* mechanism (`BUNDLE_SIZE_REFRESH`, Story #151; `MAINTAINABILITY_REFRESH`,
|
|
90
|
+
* Story #4731) are exactly what this rule produces, so generalizing kept
|
|
91
|
+
* both working unchanged.
|
|
98
92
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* i.e. the next PR — reverts to full strict enforcement automatically, so
|
|
102
|
-
* there is no lingering loosened tolerance to remember to reset (AC-3).
|
|
93
|
+
* Module-local: `resolveKindRefreshOverrides` is the public surface, and the
|
|
94
|
+
* naming rule is pinned through it rather than exported for its own sake.
|
|
103
95
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* @param {NodeJS.ProcessEnv} env
|
|
108
|
-
* @returns {{ acknowledged: boolean, overrides: string[] }}
|
|
96
|
+
* @param {string} kind
|
|
97
|
+
* @returns {string|null} null when `kind` is not a usable kind name
|
|
109
98
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
typeof raw === 'string' && /^(1|true)$/i.test(raw.trim());
|
|
114
|
-
const overrides = acknowledged
|
|
115
|
-
? [`acknowledged=true (BUNDLE_SIZE_REFRESH=${raw})`]
|
|
116
|
-
: [];
|
|
117
|
-
return { acknowledged, overrides };
|
|
99
|
+
function kindRefreshEnvVar(kind) {
|
|
100
|
+
if (typeof kind !== 'string' || kind.length === 0) return null;
|
|
101
|
+
return `${kind.toUpperCase().replace(/-/g, '_')}_REFRESH`;
|
|
118
102
|
}
|
|
119
103
|
|
|
120
104
|
/**
|
|
121
|
-
* Pure helper: resolve the one-shot
|
|
122
|
-
* (Story #
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
105
|
+
* Pure helper: resolve the one-shot baseline refresh/acknowledge flag for any
|
|
106
|
+
* ratcheted kind (Story #4802, generalizing Story #151 / Story #4731).
|
|
107
|
+
*
|
|
108
|
+
* `<KIND>_REFRESH=1` tells `check-baselines --gate <kind>` to demote this
|
|
109
|
+
* run's head-vs-base regressions to `unchanged` for this invocation only.
|
|
110
|
+
* Floors still apply — an acknowledged run can still fail on an absolute
|
|
111
|
+
* floor breach; only the ratchet-vs-base comparison is suspended.
|
|
112
|
+
*
|
|
113
|
+
* Why every kind needs this: a diff-scope baseline is an accretion of many
|
|
114
|
+
* partial runs, not one measurement. Replacing it with a single full-scope
|
|
115
|
+
* measurement necessarily produces row deltas in both directions that are
|
|
116
|
+
* arithmetic, not behavioural — so without an acknowledgment path the gate
|
|
117
|
+
* blocks precisely the correction it should encourage.
|
|
129
118
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* strength automatically.
|
|
119
|
+
* The flag is **not persisted** anywhere (no config write, no committed tag):
|
|
120
|
+
* the very next invocation without the env var reverts to full strict
|
|
121
|
+
* enforcement automatically, so there is no lingering loosened tolerance to
|
|
122
|
+
* remember to reset. The evaluate phase pairs this with a commit-tagged
|
|
123
|
+
* trigger that is likewise one-shot by construction.
|
|
136
124
|
*
|
|
137
|
-
* Accepted truthy values: `1`, `true` (case-insensitive)
|
|
138
|
-
*
|
|
125
|
+
* Accepted truthy values: `1`, `true` (case-insensitive), with surrounding
|
|
126
|
+
* whitespace trimmed. Anything else — including unset, empty, `0`, `false`,
|
|
127
|
+
* and non-string values — resolves to `acknowledged: false`.
|
|
139
128
|
*
|
|
129
|
+
* @param {string} kind
|
|
140
130
|
* @param {NodeJS.ProcessEnv} env
|
|
141
131
|
* @returns {{ acknowledged: boolean, overrides: string[] }}
|
|
142
132
|
*/
|
|
143
|
-
export function
|
|
144
|
-
const
|
|
133
|
+
export function resolveKindRefreshOverrides(kind, env) {
|
|
134
|
+
const varName = kindRefreshEnvVar(kind);
|
|
135
|
+
if (!varName) return { acknowledged: false, overrides: [] };
|
|
136
|
+
const raw = env?.[varName];
|
|
145
137
|
const acknowledged =
|
|
146
138
|
typeof raw === 'string' && /^(1|true)$/i.test(raw.trim());
|
|
147
139
|
const overrides = acknowledged
|
|
148
|
-
? [`acknowledged=true (
|
|
140
|
+
? [`acknowledged=true (${varName}=${raw})`]
|
|
149
141
|
: [];
|
|
150
142
|
return { acknowledged, overrides };
|
|
151
143
|
}
|
|
@@ -60,11 +60,10 @@ export const MAINTAINABILITY_EXCLUSIONS = Object.freeze(
|
|
|
60
60
|
// the audit-to-stories parser reuses the same regex-property scan
|
|
61
61
|
// patterns as acceptance-spec-reconciler.
|
|
62
62
|
'.agents/scripts/lib/audit-to-stories/parse-audit-md.js',
|
|
63
|
-
// escomplex: same "pattern" parse failure family — BDD scanner
|
|
64
|
-
//
|
|
65
|
-
//
|
|
63
|
+
// escomplex: same "pattern" parse failure family — the BDD scanner walks
|
|
64
|
+
// source trees with regex visitors that hit the upstream destructuring
|
|
65
|
+
// bug.
|
|
66
66
|
'.agents/scripts/lib/bdd-scenario-scanner.js',
|
|
67
|
-
'.agents/scripts/lib/codebase-snapshot.js',
|
|
68
67
|
// escomplex: same "pattern" parse failure — the wave-runner tick uses
|
|
69
68
|
// the regex-property destructuring escomplex chokes on.
|
|
70
69
|
'.agents/scripts/lib/wave-runner/tick.js',
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* bdd-scenario-scanner.js — Gherkin scenario index for /plan Phase 7.
|
|
3
3
|
*
|
|
4
|
-
* Story #2637 (sibling to #
|
|
5
|
-
*
|
|
4
|
+
* Story #2637 (sibling to #2636's file-assumption gate; the #2634 and #2635
|
|
5
|
+
* planner-grounding siblings were retired in Story #4811). The Acceptance
|
|
6
|
+
* Engineer step of
|
|
6
7
|
* `epic-plan-spec-author` currently writes ACs from Epic/Tech Spec narrative
|
|
7
8
|
* alone — it never inspects the consumer project's existing `.feature`
|
|
8
9
|
* files. Planned ACs frequently duplicate scenarios that already exist or
|
|
@@ -114,14 +114,6 @@ const KEY_MEANINGS = Object.freeze({
|
|
|
114
114
|
'Allowlist of events that fire a webhook notification.',
|
|
115
115
|
|
|
116
116
|
// planning.*
|
|
117
|
-
'planning.codebaseSnapshot.tier':
|
|
118
|
-
'Depth of the structural codebase view threaded into spec authoring.',
|
|
119
|
-
'planning.codebaseSnapshot.include':
|
|
120
|
-
'Glob patterns included in the codebase snapshot.',
|
|
121
|
-
'planning.codebaseSnapshot.exclude':
|
|
122
|
-
'Glob patterns excluded from the codebase snapshot.',
|
|
123
|
-
'planning.codebaseSnapshot.recentCommitWindow':
|
|
124
|
-
'How many recent commits the snapshot summarizes.',
|
|
125
117
|
'planning.riskHeuristics':
|
|
126
118
|
'Phrases that flag a Story as high-risk for HITL escalation.',
|
|
127
119
|
'planning.failOnSharedEditors':
|
|
@@ -56,6 +56,8 @@ import { mkdtempSync } from 'node:fs';
|
|
|
56
56
|
import os from 'node:os';
|
|
57
57
|
import path from 'node:path';
|
|
58
58
|
|
|
59
|
+
import { reapOnExit } from '../test-temp.js';
|
|
60
|
+
|
|
59
61
|
/**
|
|
60
62
|
* Cache the resolved main-checkout root per spawn cwd so the
|
|
61
63
|
* `git rev-parse` shell-out runs at most once per distinct working
|
|
@@ -223,7 +225,7 @@ function inNodeTestContext(env, execArgv) {
|
|
|
223
225
|
*
|
|
224
226
|
* @param {string} tempRoot
|
|
225
227
|
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
226
|
-
* @param {{ mkdtemp?: typeof mkdtempSync, execArgv?: string[] }} [deps]
|
|
228
|
+
* @param {{ mkdtemp?: typeof mkdtempSync, execArgv?: string[], onExit?: (fn: () => void) => void }} [deps]
|
|
227
229
|
* Injectable for tests.
|
|
228
230
|
* @returns {string}
|
|
229
231
|
*/
|
|
@@ -238,9 +240,18 @@ export function anchorTempRoot(tempRoot, env = process.env, deps = {}) {
|
|
|
238
240
|
) {
|
|
239
241
|
if (_testContextScratchDir === null) {
|
|
240
242
|
const mkdtemp = deps.mkdtemp ?? mkdtempSync;
|
|
243
|
+
// test-temp-allow: published to children below, so it must live
|
|
244
|
+
// outside the per-process suite root that this process reaps.
|
|
241
245
|
_testContextScratchDir = mkdtemp(
|
|
242
246
|
path.join(os.tmpdir(), 'mandrel-test-temp-'),
|
|
243
247
|
);
|
|
248
|
+
// Creator-only reaping (Story #4808): a process that read the root
|
|
249
|
+
// from the env returned at `scratch` above and never reaches here,
|
|
250
|
+
// so it can never remove a root its parent is still writing to.
|
|
251
|
+
reapOnExit(
|
|
252
|
+
_testContextScratchDir,
|
|
253
|
+
deps.onExit ? { onExit: deps.onExit } : {},
|
|
254
|
+
);
|
|
244
255
|
if (env === process.env) {
|
|
245
256
|
// Children spawned by this test process inherit the same scratch.
|
|
246
257
|
process.env[TEST_TEMP_ROOT_ENV] = _testContextScratchDir;
|
|
@@ -249,35 +249,19 @@ const GITHUB_SCHEMA = {
|
|
|
249
249
|
// rejected as an additional property, so a resurrected key fails loudly rather
|
|
250
250
|
// than silently doing nothing.
|
|
251
251
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
type: 'object',
|
|
261
|
-
properties: {
|
|
262
|
-
tier: { type: 'string', enum: ['skinny', 'medium'] },
|
|
263
|
-
include: {
|
|
264
|
-
type: 'array',
|
|
265
|
-
items: { type: 'string', minLength: 1 },
|
|
266
|
-
},
|
|
267
|
-
exclude: {
|
|
268
|
-
type: 'array',
|
|
269
|
-
items: { type: 'string', minLength: 1 },
|
|
270
|
-
},
|
|
271
|
-
recentCommitWindow: { type: 'integer', minimum: 1 },
|
|
272
|
-
},
|
|
273
|
-
additionalProperties: false,
|
|
274
|
-
};
|
|
252
|
+
// Story #4811: the `planning` block's structural-snapshot key was retired
|
|
253
|
+
// along with the snapshot itself. The pre-computed view it configured grounded
|
|
254
|
+
// nothing — its default include globs missed the standard monorepo layout, and
|
|
255
|
+
// its knobs only re-filtered the same matched set. Spec authoring is grounded
|
|
256
|
+
// by the author's own targeted repo retrieval plus the Phase 8
|
|
257
|
+
// `validateStoryFileAssumptions` gate, neither of which is configurable here.
|
|
258
|
+
// `planning` carries `additionalProperties: false`, so a resurrected key fails
|
|
259
|
+
// loudly; the 2.20.0 retirement migration strips it on upgrade.
|
|
275
260
|
|
|
276
261
|
const PLANNING_SCHEMA = {
|
|
277
262
|
type: 'object',
|
|
278
263
|
properties: {
|
|
279
264
|
riskHeuristics: LIST_OR_EXTENDER_OF_STRINGS,
|
|
280
|
-
codebaseSnapshot: CODEBASE_SNAPSHOT_SCHEMA,
|
|
281
265
|
// Story #4722 (superseding #4683's word-count gate) — shape-derived
|
|
282
266
|
// ceremony-lite routing. Complexity routes on the objective shape of the
|
|
283
267
|
// authored work (changes[] count, acceptance count, creates-vs-refactors
|
|
@@ -7,10 +7,7 @@
|
|
|
7
7
|
* @module lib/orchestration/check-baselines/phases/evaluate
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
resolveBundleSizeEnvOverrides,
|
|
12
|
-
resolveMaintainabilityRefreshOverrides,
|
|
13
|
-
} from '../../../baselines/env-overrides.js';
|
|
10
|
+
import { resolveKindRefreshOverrides } from '../../../baselines/env-overrides.js';
|
|
14
11
|
import { readRangeSubjectsTouchingFile } from '../../../baselines/git-base.js';
|
|
15
12
|
import {
|
|
16
13
|
checkBaselineSemantics,
|
|
@@ -91,57 +88,34 @@ function loadHeadBaseline(kind, cwd, configPath) {
|
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* never persisted, so the ratchet returns to full strength automatically on
|
|
99
|
-
* the very next run (no lingering loosened tolerance to remember to reset).
|
|
100
|
-
*
|
|
101
|
-
* No-op for every other kind.
|
|
102
|
-
*/
|
|
103
|
-
function applyBundleSizeAcknowledgment(kind, compareOutput, env) {
|
|
104
|
-
if (kind !== 'bundle-size') return { compareOutput, acknowledged: false };
|
|
105
|
-
const { acknowledged, overrides } = resolveBundleSizeEnvOverrides(env);
|
|
106
|
-
if (!acknowledged || compareOutput.regressions.length === 0) {
|
|
107
|
-
return { compareOutput, acknowledged: false };
|
|
108
|
-
}
|
|
109
|
-
Logger.warn(
|
|
110
|
-
`[bundle-size] ⚠ ${overrides.join(', ')} — ` +
|
|
111
|
-
`${compareOutput.regressions.length} regression(s) acknowledged for this run only; ` +
|
|
112
|
-
'floors still enforced. This does not persist: the next run without ' +
|
|
113
|
-
'BUNDLE_SIZE_REFRESH re-enforces the ratchet at full strength.',
|
|
114
|
-
);
|
|
115
|
-
return {
|
|
116
|
-
acknowledged: true,
|
|
117
|
-
compareOutput: {
|
|
118
|
-
...compareOutput,
|
|
119
|
-
regressions: [],
|
|
120
|
-
unchanged: [...compareOutput.unchanged, ...compareOutput.regressions],
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Resolve the maintainability refresh trigger (Story #4731). Two paths, either
|
|
127
|
-
* of which acknowledges — mirroring the bundle-size acknowledge but adding the
|
|
128
|
-
* commit-tagged trigger the breach message already documents:
|
|
91
|
+
* Resolve the one-shot refresh trigger for any ratcheted kind (Story #4802,
|
|
92
|
+
* generalizing Story #151's bundle-size env flag and Story #4731's
|
|
93
|
+
* maintainability env-or-commit-tag pair). Two paths, either of which
|
|
94
|
+
* acknowledges:
|
|
129
95
|
*
|
|
130
|
-
* 1. Env parity:
|
|
96
|
+
* 1. Env parity: `<KIND>_REFRESH=1` (the manual override) — upper-snaked,
|
|
97
|
+
* so the two pre-existing names (`BUNDLE_SIZE_REFRESH`,
|
|
98
|
+
* `MAINTAINABILITY_REFRESH`) keep working unchanged.
|
|
131
99
|
* 2. Commit tag: a commit in the compared range `<baseRef>..HEAD` whose
|
|
132
|
-
* subject contains the configured `refreshTag` AND whose diff touches
|
|
133
|
-
*
|
|
134
|
-
*
|
|
100
|
+
* subject contains the configured `refreshTag` AND whose diff touches
|
|
101
|
+
* that kind's baseline file. One-shot by construction — once merged, the
|
|
102
|
+
* refreshed baseline becomes the base and the tag leaves the range.
|
|
135
103
|
*
|
|
136
104
|
* The tag is matched as a plain substring of a conventional commit subject, so
|
|
137
105
|
* commitlint stays satisfied (e.g. `chore(baselines): baseline-refresh: …`).
|
|
138
106
|
*
|
|
107
|
+
* Fails closed: a kind whose baseline path is neither configured nor present
|
|
108
|
+
* in `DEFAULT_BASELINE_PATHS` simply skips the commit-tag path rather than
|
|
109
|
+
* throwing, leaving the run un-acknowledged.
|
|
110
|
+
*
|
|
139
111
|
* @returns {{ triggered: boolean, reasons: string[] }}
|
|
140
112
|
*/
|
|
141
|
-
function
|
|
113
|
+
function resolveRefreshTrigger({ kind, gateBlock, cmp, cwd, env }) {
|
|
142
114
|
const reasons = [];
|
|
143
|
-
const { acknowledged: envAck, overrides } =
|
|
144
|
-
|
|
115
|
+
const { acknowledged: envAck, overrides } = resolveKindRefreshOverrides(
|
|
116
|
+
kind,
|
|
117
|
+
env,
|
|
118
|
+
);
|
|
145
119
|
if (envAck) reasons.push(...overrides);
|
|
146
120
|
|
|
147
121
|
const baseRef = cmp?.baseRef ?? null;
|
|
@@ -154,15 +128,17 @@ function resolveMaintainabilityRefreshTrigger({ gateBlock, cmp, cwd, env }) {
|
|
|
154
128
|
typeof gateBlock?.baselinePath === 'string' &&
|
|
155
129
|
gateBlock.baselinePath.length
|
|
156
130
|
? gateBlock.baselinePath
|
|
157
|
-
: DEFAULT_BASELINE_PATHS
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
131
|
+
: DEFAULT_BASELINE_PATHS[kind];
|
|
132
|
+
if (typeof baselinePath === 'string' && baselinePath.length) {
|
|
133
|
+
const subjects = readRangeSubjectsTouchingFile(baseRef, baselinePath, {
|
|
134
|
+
cwd,
|
|
135
|
+
});
|
|
136
|
+
const match = subjects.find((s) => s.includes(refreshTag));
|
|
137
|
+
if (match) {
|
|
138
|
+
reasons.push(
|
|
139
|
+
`refresh commit "${match}" (subject contains ${JSON.stringify(refreshTag)}, touches ${baselinePath})`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
166
142
|
}
|
|
167
143
|
}
|
|
168
144
|
|
|
@@ -170,26 +146,24 @@ function resolveMaintainabilityRefreshTrigger({ gateBlock, cmp, cwd, env }) {
|
|
|
170
146
|
}
|
|
171
147
|
|
|
172
148
|
/**
|
|
173
|
-
* One-shot
|
|
174
|
-
* (env flag OR a `baseline-refresh:`-tagged range commit
|
|
175
|
-
* demote every
|
|
176
|
-
* run only — floors still apply, so a row below its
|
|
177
|
-
* The trigger is read fresh every run and never persisted:
|
|
178
|
-
* refreshed baseline is the new base and the tag leaves the
|
|
179
|
-
* ratchet returns to full strength automatically.
|
|
149
|
+
* One-shot baseline refresh/acknowledge for any ratcheted kind (Story #4802).
|
|
150
|
+
* When triggered (env flag OR a `baseline-refresh:`-tagged range commit
|
|
151
|
+
* touching that kind's baseline), demote every head-vs-base regression to
|
|
152
|
+
* `unchanged` for this run only — floors still apply, so a row below its floor
|
|
153
|
+
* still breaches. The trigger is read fresh every run and never persisted:
|
|
154
|
+
* post-merge the refreshed baseline is the new base and the tag leaves the
|
|
155
|
+
* range, so the ratchet returns to full strength automatically.
|
|
180
156
|
*
|
|
181
|
-
*
|
|
157
|
+
* A no-op absent a trigger, so an unacknowledged run of any kind reports its
|
|
158
|
+
* regressions exactly as before.
|
|
182
159
|
*/
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
return { compareOutput, acknowledged: false };
|
|
186
|
-
}
|
|
187
|
-
const { triggered, reasons } = resolveMaintainabilityRefreshTrigger(ctx);
|
|
160
|
+
function applyRefreshAcknowledgment(kind, compareOutput, ctx) {
|
|
161
|
+
const { triggered, reasons } = resolveRefreshTrigger({ ...ctx, kind });
|
|
188
162
|
if (!triggered || compareOutput.regressions.length === 0) {
|
|
189
163
|
return { compareOutput, acknowledged: false };
|
|
190
164
|
}
|
|
191
165
|
Logger.warn(
|
|
192
|
-
`[
|
|
166
|
+
`[${kind}] ⚠ ${reasons.join('; ')} — ` +
|
|
193
167
|
`${compareOutput.regressions.length} regression(s) acknowledged for this run only; ` +
|
|
194
168
|
'floors still enforced. This does not persist: once the refresh is the ' +
|
|
195
169
|
'new base the ratchet re-enforces at full strength.',
|
|
@@ -274,14 +248,14 @@ export async function evaluateKind({
|
|
|
274
248
|
rawCompare,
|
|
275
249
|
gateBlock.tolerance ?? null,
|
|
276
250
|
);
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
);
|
|
283
|
-
const compareOutput =
|
|
284
|
-
const acknowledged =
|
|
251
|
+
const ack = applyRefreshAcknowledgment(kind, toleratedCompare, {
|
|
252
|
+
gateBlock,
|
|
253
|
+
cmp,
|
|
254
|
+
cwd,
|
|
255
|
+
env,
|
|
256
|
+
});
|
|
257
|
+
const compareOutput = ack.compareOutput;
|
|
258
|
+
const acknowledged = ack.acknowledged;
|
|
285
259
|
return buildGateReport({
|
|
286
260
|
kind,
|
|
287
261
|
gateBlock,
|
|
@@ -44,18 +44,26 @@ compare) over every configured gate, with centralised friction emission and
|
|
|
44
44
|
aggregated exit codes.
|
|
45
45
|
|
|
46
46
|
Env vars:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"unchanged" for this run only
|
|
50
|
-
enforced
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
<KIND>_REFRESH=1 One-shot acknowledge for a deliberate baseline
|
|
48
|
+
refresh of that kind: demotes its head-vs-base
|
|
49
|
+
regressions to "unchanged" for this run only. Floors
|
|
50
|
+
are STILL enforced, so a genuine breach is still
|
|
51
|
+
caught. The kind name is upper-snaked, e.g.
|
|
52
|
+
COVERAGE_REFRESH, CRAP_REFRESH, DUPLICATION_REFRESH,
|
|
53
|
+
MAINTAINABILITY_REFRESH, BUNDLE_SIZE_REFRESH.
|
|
54
|
+
Never persisted — the next run without the flag
|
|
55
|
+
re-enforces the ratchet at full strength.
|
|
56
|
+
|
|
57
|
+
Equivalent commit-tagged trigger: a commit in the
|
|
58
|
+
compared range whose subject contains the gate's
|
|
59
|
+
'refreshTag' (default 'baseline-refresh:') AND whose
|
|
60
|
+
diff touches that kind's baseline file. One-shot by
|
|
61
|
+
construction — once merged the refreshed baseline is
|
|
62
|
+
the new base and the tag leaves the range.
|
|
63
|
+
|
|
64
|
+
Use these when replacing a diff-scope baseline with a
|
|
65
|
+
full-scope measurement: the resulting row deltas are
|
|
66
|
+
arithmetic, not behavioural.
|
|
59
67
|
|
|
60
68
|
Exit codes:
|
|
61
69
|
0 every enabled gate passes
|
|
@@ -60,8 +60,10 @@ import { isObjectPathEntry } from './task-body-validator.js';
|
|
|
60
60
|
/**
|
|
61
61
|
* Default git probe — returns `true` when `path` exists at
|
|
62
62
|
* `baseBranchRef`. Mirrors the existence check used by
|
|
63
|
-
* {@link ./ticket-validator.js#validateAcFreshness}
|
|
64
|
-
*
|
|
63
|
+
* {@link ./ticket-validator.js#validateAcFreshness} so both gates share
|
|
64
|
+
* semantics. (Story #4811 deleted the third sharer, `spec-freshness.js`,
|
|
65
|
+
* along with the codebase snapshot it grounded; this gate — Phase 8 — is
|
|
66
|
+
* now the grounding gate and its behaviour is unchanged.)
|
|
65
67
|
*
|
|
66
68
|
* @param {{ baseBranchRef: string, path: string, cwd?: string }} opts
|
|
67
69
|
* @returns {boolean}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Each listener in this directory subscribes to one or more lifecycle bus
|
|
4
4
|
events and performs a single side effect. The full close-tail roster and
|
|
5
|
-
event taxonomy live in
|
|
5
|
+
event taxonomy live in
|
|
6
|
+
[`docs/LIFECYCLE.md`](https://github.com/dsj1984/mandrel/blob/main/docs/LIFECYCLE.md)
|
|
6
7
|
— that document is the SSOT. This README only indexes the **files that still
|
|
7
8
|
live in this folder**.
|
|
8
9
|
|
|
@@ -48,15 +48,16 @@ const SOURCE_TICKET_FETCH_CONCURRENCY = 4;
|
|
|
48
48
|
* body and ship the raw seed on `seed.content` instead — the budget bounded
|
|
49
49
|
* a field that never left the function.
|
|
50
50
|
*
|
|
51
|
-
*
|
|
52
|
-
* (~
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* under the session budget. The test suite asserts
|
|
59
|
-
* under this value — raise it only with a measured
|
|
51
|
+
* A measured seed-mode envelope on this repo is ~120 KB, dominated by the
|
|
52
|
+
* digest-first `docsContext` (~63 KB inline digest) and the rendered
|
|
53
|
+
* `systemPrompts` (~54 KB); every other field is under 1 KB. Story #4811
|
|
54
|
+
* retired the tier-capped codebase snapshot that used to sit alongside them
|
|
55
|
+
* (~35 KB skinny here). The seed itself is operator-supplied, carried
|
|
56
|
+
* verbatim, and is the only unbounded contributor. 256 KB (~64K tokens at the
|
|
57
|
+
* ≈4-chars/token estimate) leaves roughly 2× headroom over that measurement
|
|
58
|
+
* while staying well under the session budget. The test suite asserts
|
|
59
|
+
* serialized envelopes stay under this value — raise it only with a measured
|
|
60
|
+
* justification.
|
|
60
61
|
*/
|
|
61
62
|
export const PLAN_CONTEXT_ENVELOPE_BYTE_CEILING = 256_000;
|
|
62
63
|
|
|
@@ -112,9 +113,9 @@ function assertPlanContextWithinCeiling(envelope, opts = {}) {
|
|
|
112
113
|
`[plan-context] the assembled "${envelope?.mode}" envelope is ` +
|
|
113
114
|
`${Math.round(bytes / 1024)} KB, over the ` +
|
|
114
115
|
`${Math.round(ceiling / 1024)} KB planner-context ceiling. Largest ` +
|
|
115
|
-
`fields: ${largest}. Trim the seed, plan fewer --tickets source
|
|
116
|
-
'in one run
|
|
117
|
-
'
|
|
116
|
+
`fields: ${largest}. Trim the seed, or plan fewer --tickets source ` +
|
|
117
|
+
'issues in one run. Raising the ceiling needs a measured ' +
|
|
118
|
+
'justification — see PLAN_CONTEXT_ENVELOPE_BYTE_CEILING.',
|
|
118
119
|
);
|
|
119
120
|
}
|
|
120
121
|
|
|
@@ -890,7 +891,6 @@ async function buildSeedFileModeEnvelope({
|
|
|
890
891
|
),
|
|
891
892
|
duplicates,
|
|
892
893
|
docsContext,
|
|
893
|
-
codebaseSnapshot: authoring.codebaseSnapshot,
|
|
894
894
|
bddRunner: authoring.bddRunner,
|
|
895
895
|
bddScenarios: authoring.bddScenarios,
|
|
896
896
|
memoryFreshness: authoring.memoryFreshness,
|
|
@@ -1052,7 +1052,6 @@ async function buildTicketsModeEnvelope({
|
|
|
1052
1052
|
),
|
|
1053
1053
|
duplicates,
|
|
1054
1054
|
docsContext,
|
|
1055
|
-
codebaseSnapshot: authoring.codebaseSnapshot,
|
|
1056
1055
|
bddRunner: authoring.bddRunner,
|
|
1057
1056
|
bddScenarios: authoring.bddScenarios,
|
|
1058
1057
|
memoryFreshness: authoring.memoryFreshness,
|