mandrel 1.80.0 → 1.81.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/configuration.md +7 -0
- package/.agents/docs/quality-gates.md +61 -0
- package/.agents/scripts/lib/baselines/env-overrides.js +35 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +47 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +7 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/pipeline.js +1 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +2 -1
- package/docs/CHANGELOG.md +7 -0
- package/package.json +1 -1
|
@@ -627,6 +627,12 @@ No tier-specific knobs beyond the common shape.
|
|
|
627
627
|
| --------- | -------- | ------- | ---------------------------------------------------------------------------------------- |
|
|
628
628
|
| `bundles` | No | `[]` | Array of `{ name, path, limit }` entries (e.g. `{ "name": "app", "path": "dist/app.js", "limit": "100kB" }`). |
|
|
629
629
|
|
|
630
|
+
Unlike `crap` / `maintainability`, this gate has no `refreshTag` config
|
|
631
|
+
field — there is no scorer to regenerate the baseline from source, so the
|
|
632
|
+
one-shot refresh/acknowledge mechanism is an env var, not a config knob.
|
|
633
|
+
See [`.agents/docs/quality-gates.md` § Bundle-size ratchet](quality-gates.md#bundle-size-ratchet--one-shot-refreshacknowledge-story-151)
|
|
634
|
+
for `BUNDLE_SIZE_REFRESH=1` usage.
|
|
635
|
+
|
|
630
636
|
#### `delivery.quality.formatAutofix`
|
|
631
637
|
|
|
632
638
|
| Field | Required | Default | Purpose |
|
|
@@ -791,6 +797,7 @@ the lint ratchet, and the CRAP/MI gates.
|
|
|
791
797
|
| `baselines/lint.json` | `lint-baseline.js` | `node .agents/scripts/lint-baseline.js capture` |
|
|
792
798
|
| `baselines/crap.json` | `update-crap-baseline.js` | `npm run crap:update` |
|
|
793
799
|
| `baselines/maintainability.json` | `update-maintainability-baseline.js` | `npm run maintainability:update` |
|
|
800
|
+
| `baselines/bundle-size.json` | consumer's own build/measure step | Commit the build's measured sizes; for an intentional growth, run the check with `BUNDLE_SIZE_REFRESH=1` (see [Bundle-size ratchet](quality-gates.md#bundle-size-ratchet--one-shot-refreshacknowledge-story-151)) |
|
|
794
801
|
|
|
795
802
|
These files are the contract. They are read by every gate (Story close, push
|
|
796
803
|
hook, CI) and are regenerated only via tagged `baseline-refresh:` commits
|
|
@@ -452,6 +452,67 @@ watch loop — an unjustified baseline ratchet is no longer caught by CI.
|
|
|
452
452
|
|
|
453
453
|
---
|
|
454
454
|
|
|
455
|
+
## Bundle-size ratchet — one-shot refresh/acknowledge (Story #151)
|
|
456
|
+
|
|
457
|
+
> Baseline envelope, axes, and component model: see the
|
|
458
|
+
> [Baseline reference](#baseline-reference) section below.
|
|
459
|
+
|
|
460
|
+
`check-baselines --gate bundle-size` is a **strict** ratchet: it diffs the
|
|
461
|
+
branch's committed `baselines/bundle-size.json` (head) against the base
|
|
462
|
+
ref's copy (`origin/main` by default) using the gate's configured
|
|
463
|
+
`tolerance`, and separately checks the head aggregate against `floors`.
|
|
464
|
+
Unlike `coverage` / `crap` / `maintainability`, bundle-size has **no
|
|
465
|
+
scorer of its own** — the measured `rawKb` / `gzippedKb` numbers come from
|
|
466
|
+
whatever build step the consumer already runs, not a source-tree rescan —
|
|
467
|
+
so there is no `refreshBaseline({ kind: 'bundle-size', ... })` path to
|
|
468
|
+
regenerate a "corrected" baseline the way `npm run crap:update` does.
|
|
469
|
+
|
|
470
|
+
This makes an **intentional** bundle-size growth (a framework major bump,
|
|
471
|
+
a new dependency, an SSR runtime swap) impossible to land cleanly with the
|
|
472
|
+
usual levers: permanently raising `tolerance` in `.agentrc.json` disables
|
|
473
|
+
the ratchet for every *future* PR too, not just the one that legitimately
|
|
474
|
+
grew.
|
|
475
|
+
|
|
476
|
+
### `BUNDLE_SIZE_REFRESH=1`
|
|
477
|
+
|
|
478
|
+
Set the environment variable for the one CI/local run that needs to land
|
|
479
|
+
the growth:
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
BUNDLE_SIZE_REFRESH=1 npm run bundle-size:check
|
|
483
|
+
# or, calling the dispatcher directly:
|
|
484
|
+
BUNDLE_SIZE_REFRESH=1 node .agents/scripts/check-baselines.js --gate bundle-size
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
When set (`1` or `true`, case-insensitive), every `bundle-size`
|
|
488
|
+
head-vs-base regression is demoted to `unchanged` **for that invocation
|
|
489
|
+
only** — the gate compares head-vs-head in effect, so it passes even
|
|
490
|
+
though the committed baseline grew. **Floors still apply**: an
|
|
491
|
+
acknowledged PR can still fail if the head aggregate breaches the
|
|
492
|
+
configured `floors` budget, so a genuinely runaway regression isn't
|
|
493
|
+
silently waved through under the guise of "intentional".
|
|
494
|
+
|
|
495
|
+
Commit the regenerated `baselines/bundle-size.json` (reflecting the real,
|
|
496
|
+
larger sizes) in the same PR so the new numbers become the base for the
|
|
497
|
+
*next* PR's diff.
|
|
498
|
+
|
|
499
|
+
### The ratchet returns to full strength automatically
|
|
500
|
+
|
|
501
|
+
`BUNDLE_SIZE_REFRESH` is read fresh on every invocation and is **never
|
|
502
|
+
persisted** — no config write, no committed tag, no lingering state. The
|
|
503
|
+
very next `check-baselines --gate bundle-size` run (i.e. the next PR),
|
|
504
|
+
without the env var set, re-enforces the ratchet at full strength against
|
|
505
|
+
the now-larger committed baseline. There is nothing to remember to reset.
|
|
506
|
+
|
|
507
|
+
This mirrors the `CRAP_TOLERANCE` env-override precedent (see
|
|
508
|
+
[CRAP gate — Consumer onboarding](#crap-gate--consumer-onboarding) above),
|
|
509
|
+
but as a true one-shot acknowledgment rather than a run-scoped tolerance
|
|
510
|
+
override: `CRAP_TOLERANCE` changes the *threshold*, `BUNDLE_SIZE_REFRESH`
|
|
511
|
+
demotes the *outcome* of an already-flagged regression, which is the
|
|
512
|
+
correct shape for a gate with no rescoring path of its own.
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
455
516
|
## HITL blocker escalation
|
|
456
517
|
|
|
457
518
|
`risk::high` is informational/planning metadata only. Runtime execution
|
|
@@ -82,6 +82,41 @@ export function resolveCrapEnvOverrides(crapConfig, env) {
|
|
|
82
82
|
return { newMethodCeiling, tolerance, refreshTag, overrides };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Pure helper: resolve the one-shot bundle-size refresh/acknowledge flag
|
|
87
|
+
* (Story #151). Unlike `coverage` / `crap` / `maintainability`, the
|
|
88
|
+
* bundle-size gate has no scorer of its own — the measured sizes come from
|
|
89
|
+
* a build step the operator already runs, not a source-tree rescan — so
|
|
90
|
+
* there is no `refreshBaseline({ kind: 'bundle-size', ... })` path to
|
|
91
|
+
* regenerate a "corrected" baseline. Instead, `BUNDLE_SIZE_REFRESH=1`
|
|
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.
|
|
98
|
+
*
|
|
99
|
+
* The flag is **not persisted** anywhere (no config write, no committed
|
|
100
|
+
* tag): the very next `check-baselines` invocation without the env var —
|
|
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).
|
|
103
|
+
*
|
|
104
|
+
* Accepted truthy values: `1`, `true` (case-insensitive). Anything else
|
|
105
|
+
* (including unset/empty) resolves to `acknowledged: false`.
|
|
106
|
+
*
|
|
107
|
+
* @param {NodeJS.ProcessEnv} env
|
|
108
|
+
* @returns {{ acknowledged: boolean, overrides: string[] }}
|
|
109
|
+
*/
|
|
110
|
+
export function resolveBundleSizeEnvOverrides(env) {
|
|
111
|
+
const raw = env?.BUNDLE_SIZE_REFRESH;
|
|
112
|
+
const acknowledged =
|
|
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 };
|
|
118
|
+
}
|
|
119
|
+
|
|
85
120
|
/**
|
|
86
121
|
* Pure helper: resolve the effective MI tolerance by layering precedence:
|
|
87
122
|
* 1. `CRAP_TOLERANCE` env-var (CI override — the baseline-refresh-
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* @module lib/orchestration/check-baselines/phases/evaluate
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { resolveBundleSizeEnvOverrides } from '../../../baselines/env-overrides.js';
|
|
10
11
|
import { checkKernelVersion } from '../../../baselines/kernel.js';
|
|
11
12
|
import * as reader from '../../../baselines/reader.js';
|
|
13
|
+
import { Logger } from '../../../Logger.js';
|
|
12
14
|
import { applyTolerance, evaluateCompare, runCompareStage } from './compare.js';
|
|
13
15
|
import { applyFloors, flattenBreaches } from './floors.js';
|
|
14
16
|
|
|
@@ -22,6 +24,38 @@ function loadHeadBaseline(kind, cwd, configPath) {
|
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* One-shot bundle-size refresh/acknowledge (Story #151). When
|
|
29
|
+
* `BUNDLE_SIZE_REFRESH=1` is set, demote every `bundle-size` regression to
|
|
30
|
+
* `unchanged` for this run only — floors still apply, so a genuine budget
|
|
31
|
+
* breach is still caught. The flag is read fresh on every invocation and
|
|
32
|
+
* never persisted, so the ratchet returns to full strength automatically on
|
|
33
|
+
* the very next run (no lingering loosened tolerance to remember to reset).
|
|
34
|
+
*
|
|
35
|
+
* No-op for every other kind.
|
|
36
|
+
*/
|
|
37
|
+
function applyBundleSizeAcknowledgment(kind, compareOutput, env) {
|
|
38
|
+
if (kind !== 'bundle-size') return { compareOutput, acknowledged: false };
|
|
39
|
+
const { acknowledged, overrides } = resolveBundleSizeEnvOverrides(env);
|
|
40
|
+
if (!acknowledged || compareOutput.regressions.length === 0) {
|
|
41
|
+
return { compareOutput, acknowledged: false };
|
|
42
|
+
}
|
|
43
|
+
Logger.warn(
|
|
44
|
+
`[bundle-size] ⚠ ${overrides.join(', ')} — ` +
|
|
45
|
+
`${compareOutput.regressions.length} regression(s) acknowledged for this run only; ` +
|
|
46
|
+
'floors still enforced. This does not persist: the next run without ' +
|
|
47
|
+
'BUNDLE_SIZE_REFRESH re-enforces the ratchet at full strength.',
|
|
48
|
+
);
|
|
49
|
+
return {
|
|
50
|
+
acknowledged: true,
|
|
51
|
+
compareOutput: {
|
|
52
|
+
...compareOutput,
|
|
53
|
+
regressions: [],
|
|
54
|
+
unchanged: [...compareOutput.unchanged, ...compareOutput.regressions],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
25
59
|
function buildGateReport({
|
|
26
60
|
kind,
|
|
27
61
|
gateBlock,
|
|
@@ -30,6 +64,7 @@ function buildGateReport({
|
|
|
30
64
|
breaches,
|
|
31
65
|
compareOutput,
|
|
32
66
|
cmp,
|
|
67
|
+
acknowledged,
|
|
33
68
|
}) {
|
|
34
69
|
const kernel = checkKernelVersion(kind, baseline.kernelVersion);
|
|
35
70
|
return {
|
|
@@ -50,6 +85,7 @@ function buildGateReport({
|
|
|
50
85
|
regressionCount: compareOutput.regressions.length,
|
|
51
86
|
baseRef: cmp.baseRef ?? null,
|
|
52
87
|
generatedAt: baseline.generatedAt,
|
|
88
|
+
acknowledged,
|
|
53
89
|
};
|
|
54
90
|
}
|
|
55
91
|
|
|
@@ -59,6 +95,7 @@ export async function evaluateKind({
|
|
|
59
95
|
scope,
|
|
60
96
|
cwd,
|
|
61
97
|
configPath,
|
|
98
|
+
env = process.env,
|
|
62
99
|
}) {
|
|
63
100
|
const headLoad = loadHeadBaseline(kind, cwd, configPath);
|
|
64
101
|
if (headLoad.schemaError) return { kind, schemaError: headLoad.schemaError };
|
|
@@ -67,7 +104,15 @@ export async function evaluateKind({
|
|
|
67
104
|
const breaches = flattenBreaches(findings);
|
|
68
105
|
const cmp = await evaluateCompare({ kind, gateBlock, scope, cwd });
|
|
69
106
|
const rawCompare = runCompareStage(baseline, cmp);
|
|
70
|
-
const
|
|
107
|
+
const toleratedCompare = applyTolerance(
|
|
108
|
+
rawCompare,
|
|
109
|
+
gateBlock.tolerance ?? null,
|
|
110
|
+
);
|
|
111
|
+
const { compareOutput, acknowledged } = applyBundleSizeAcknowledgment(
|
|
112
|
+
kind,
|
|
113
|
+
toleratedCompare,
|
|
114
|
+
env,
|
|
115
|
+
);
|
|
71
116
|
return buildGateReport({
|
|
72
117
|
kind,
|
|
73
118
|
gateBlock,
|
|
@@ -76,5 +121,6 @@ export async function evaluateKind({
|
|
|
76
121
|
breaches,
|
|
77
122
|
compareOutput,
|
|
78
123
|
cmp,
|
|
124
|
+
acknowledged,
|
|
79
125
|
});
|
|
80
126
|
}
|
|
@@ -43,6 +43,13 @@ Unified baseline dispatcher. Per-kind pipeline (schema → floor → tolerance
|
|
|
43
43
|
compare) over every configured gate, with centralised friction emission and
|
|
44
44
|
aggregated exit codes.
|
|
45
45
|
|
|
46
|
+
Env vars:
|
|
47
|
+
BUNDLE_SIZE_REFRESH=1 One-shot acknowledge for an intentional bundle-size
|
|
48
|
+
growth: demotes bundle-size regressions to
|
|
49
|
+
"unchanged" for this run only (floors still
|
|
50
|
+
enforced). Never persisted — the next run without
|
|
51
|
+
this flag re-enforces the ratchet.
|
|
52
|
+
|
|
46
53
|
Exit codes:
|
|
47
54
|
0 every enabled gate passes
|
|
48
55
|
1 any floor breach
|
|
@@ -59,7 +59,7 @@ function dispatchPerKind({ wanted, quality, env, cwd, configPath }) {
|
|
|
59
59
|
wanted.map((kind) => {
|
|
60
60
|
const gateBlock = quality.gates[kind];
|
|
61
61
|
const scope = resolveDispatchScope({ kind, quality, env });
|
|
62
|
-
return evaluateKind({ kind, gateBlock, scope, cwd, configPath });
|
|
62
|
+
return evaluateKind({ kind, gateBlock, scope, cwd, configPath, env });
|
|
63
63
|
}),
|
|
64
64
|
);
|
|
65
65
|
}
|
|
@@ -27,7 +27,8 @@ function formatGateLine(g) {
|
|
|
27
27
|
? ''
|
|
28
28
|
: ` [kernel drift ${g.kernelBaseline} → ${g.kernelCurrent}]`;
|
|
29
29
|
const baseRef = g.baseRef ? ` [baseRef=${g.baseRef}]` : '';
|
|
30
|
-
|
|
30
|
+
const ack = g.acknowledged ? ' [ACKNOWLEDGED — this run only]' : '';
|
|
31
|
+
return ` - ${g.kind}: ${status}${drift}${baseRef}${ack}`;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function formatViolationLine(component, v) {
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.81.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.80.0...mandrel-v1.81.0) (2026-07-01)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
* **baselines:** add one-shot BUNDLE_SIZE_REFRESH acknowledge for the bundle-size ratchet ([#4309](https://github.com/dsj1984/mandrel/issues/4309)) ([bc466b9](https://github.com/dsj1984/mandrel/commit/bc466b98f6c7521c3a24bbb4d93ee5169c163839))
|
|
11
|
+
|
|
5
12
|
## [1.80.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.79.0...mandrel-v1.80.0) (2026-07-01)
|
|
6
13
|
|
|
7
14
|
|
package/package.json
CHANGED