mandrel 2.62.0 → 2.63.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 +0 -1
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +1 -1
- package/.agents/schemas/baselines/baseline-envelope.schema.json +2 -19
- package/.agents/schemas/baselines/bundle-size.schema.json +0 -24
- package/.agents/schemas/baselines/coverage.schema.json +0 -26
- package/.agents/schemas/baselines/crap.schema.json +0 -28
- package/.agents/schemas/baselines/duplication.schema.json +0 -38
- package/.agents/schemas/baselines/maintainability.schema.json +0 -45
- package/.agents/schemas/baselines/mutation.schema.json +0 -28
- package/.agents/scripts/check-dead-exports.js +1 -1
- package/.agents/scripts/lib/audit-baselines/headroom.js +1 -1
- package/.agents/scripts/lib/audit-baselines/kinds.js +12 -7
- package/.agents/scripts/lib/audit-baselines/rollup.js +41 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +14 -3
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +1 -1
- package/.agents/scripts/lib/baselines/envelope.js +5 -50
- package/.agents/scripts/lib/baselines/merge-envelopes.js +16 -79
- package/.agents/scripts/lib/baselines/reader.js +7 -7
- package/.agents/scripts/lib/baselines/refresh-service.js +2 -7
- package/.agents/scripts/lib/baselines/writer.js +16 -45
- package/.agents/scripts/lib/coverage-baseline.js +0 -2
- package/.agents/scripts/lib/cyclomatic-ceiling.js +6 -18
- package/.agents/scripts/lib/git/sync-from-base.js +1 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +20 -27
- package/.agents/scripts/merge-baseline.js +13 -11
- package/.agents/workflows/audit-baselines.md +2 -1
- package/docs/CHANGELOG.md +11 -0
- package/lib/cli/registry.js +1 -1
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.63.0-baseline-merge-queue-shape.js +96 -0
- package/package.json +1 -1
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure 3-way merge of baseline envelopes by row identity. A line-based git
|
|
3
|
-
* merge
|
|
4
|
-
* both sides into a row set no scorer produced.
|
|
3
|
+
* merge conflicts on disjoint rows that sit adjacent in sort order, and can
|
|
4
|
+
* splice both sides into a row set no scorer produced.
|
|
5
5
|
*
|
|
6
6
|
* Per identity the side that differs from base wins; both differing is a
|
|
7
|
-
* conflict; absence is a value.
|
|
8
|
-
* merged rows, never merged. Identity comes from the kind's `rowIdentity`,
|
|
7
|
+
* conflict; absence is a value. Identity comes from the kind's `rowIdentity`,
|
|
9
8
|
* never `keyField`. No I/O: the driver validates the result.
|
|
10
9
|
*
|
|
11
10
|
* @module lib/baselines/merge-envelopes
|
|
@@ -15,8 +14,8 @@ import { deepEqual } from '../json-utils.js';
|
|
|
15
14
|
import { KNOWN_KINDS } from './envelope.js';
|
|
16
15
|
import { getKindModule } from './kernel.js';
|
|
17
16
|
|
|
18
|
-
/**
|
|
19
|
-
const
|
|
17
|
+
/** Merged by identity, not as a stamp. */
|
|
18
|
+
const ROW_KEY = 'rows';
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
21
|
* Kind from `$schema`, or `null` for anything not a known per-kind envelope.
|
|
@@ -76,20 +75,6 @@ function indexRows(rows, rowIdentity, side) {
|
|
|
76
75
|
return out;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
/**
|
|
80
|
-
* @param {string|undefined} ours
|
|
81
|
-
* @param {string|undefined} theirs
|
|
82
|
-
* @returns {string|undefined}
|
|
83
|
-
*/
|
|
84
|
-
function laterStamp(ours, theirs) {
|
|
85
|
-
if (typeof ours !== 'string') return theirs;
|
|
86
|
-
if (typeof theirs !== 'string') return ours;
|
|
87
|
-
const a = Date.parse(ours);
|
|
88
|
-
const b = Date.parse(theirs);
|
|
89
|
-
if (Number.isNaN(a) || Number.isNaN(b)) return ours > theirs ? ours : theirs;
|
|
90
|
-
return a >= b ? ours : theirs;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
78
|
/**
|
|
94
79
|
* 3-way merge of envelope stamps; a double bump to different values is a
|
|
95
80
|
* real conflict (two scorers).
|
|
@@ -102,7 +87,7 @@ function laterStamp(ours, theirs) {
|
|
|
102
87
|
function mergeStamps(base, ours, theirs) {
|
|
103
88
|
const keys = new Set(
|
|
104
89
|
[...Object.keys(ours), ...Object.keys(theirs), ...Object.keys(base)].filter(
|
|
105
|
-
(k) =>
|
|
90
|
+
(k) => k !== ROW_KEY,
|
|
106
91
|
),
|
|
107
92
|
);
|
|
108
93
|
const merged = {};
|
|
@@ -170,15 +155,13 @@ function mergeRowSets({ baseEnv, ours, theirs, rowIdentity }) {
|
|
|
170
155
|
}
|
|
171
156
|
|
|
172
157
|
/**
|
|
173
|
-
* `base` is `null` when the file was added on both sides.
|
|
174
|
-
* defaults to `[]`, matching what a refresh writes.
|
|
158
|
+
* `base` is `null` when the file was added on both sides.
|
|
175
159
|
*
|
|
176
160
|
* @param {{
|
|
177
161
|
* base: object|null,
|
|
178
162
|
* ours: object,
|
|
179
163
|
* theirs: object,
|
|
180
164
|
* kind?: string,
|
|
181
|
-
* components?: Array<object>,
|
|
182
165
|
* }} params
|
|
183
166
|
* @returns {{
|
|
184
167
|
* kind: string,
|
|
@@ -186,13 +169,7 @@ function mergeRowSets({ baseEnv, ours, theirs, rowIdentity }) {
|
|
|
186
169
|
* conflicts: Array<{scope: string, identity: string, base?: unknown, ours?: unknown, theirs?: unknown}>,
|
|
187
170
|
* }}
|
|
188
171
|
*/
|
|
189
|
-
export function mergeEnvelopes({
|
|
190
|
-
base,
|
|
191
|
-
ours,
|
|
192
|
-
theirs,
|
|
193
|
-
kind,
|
|
194
|
-
components = [],
|
|
195
|
-
} = {}) {
|
|
172
|
+
export function mergeEnvelopes({ base, ours, theirs, kind } = {}) {
|
|
196
173
|
const resolvedKind =
|
|
197
174
|
kind ?? kindFromEnvelope(ours) ?? kindFromEnvelope(theirs);
|
|
198
175
|
if (!resolvedKind) {
|
|
@@ -217,12 +194,7 @@ export function mergeEnvelopes({
|
|
|
217
194
|
theirs ?? {},
|
|
218
195
|
);
|
|
219
196
|
|
|
220
|
-
const envelope = {
|
|
221
|
-
...stamps,
|
|
222
|
-
generatedAt: laterStamp(ours?.generatedAt, theirs?.generatedAt),
|
|
223
|
-
rollup: mod.rollup(sortedRows, components),
|
|
224
|
-
rows: sortedRows,
|
|
225
|
-
};
|
|
197
|
+
const envelope = { ...stamps, rows: sortedRows };
|
|
226
198
|
|
|
227
199
|
return {
|
|
228
200
|
kind: resolvedKind,
|
|
@@ -251,41 +223,15 @@ function orderKeys(obj, order) {
|
|
|
251
223
|
}
|
|
252
224
|
|
|
253
225
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* and wrongly.
|
|
257
|
-
*
|
|
258
|
-
* @param {Array<object>} rows
|
|
259
|
-
* @returns {{ '*': { filesAboveCeiling: number, methodsAboveCeiling: number, maxCyclomatic: number } }}
|
|
260
|
-
*/
|
|
261
|
-
function cyclomaticRollup(rows) {
|
|
262
|
-
let methods = 0;
|
|
263
|
-
let max = 0;
|
|
264
|
-
for (const row of rows) {
|
|
265
|
-
methods += Number(row?.methodsAboveCeiling ?? 0);
|
|
266
|
-
const rowMax = Number(row?.maxCyclomatic ?? 0);
|
|
267
|
-
if (rowMax > max) max = rowMax;
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
'*': {
|
|
271
|
-
filesAboveCeiling: rows.length,
|
|
272
|
-
methodsAboveCeiling: methods,
|
|
273
|
-
maxCyclomatic: max,
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Row-set baselines that are not per-kind envelopes. `rollup: null` is real:
|
|
280
|
-
* the dead-export generator writes none. `keyOrder` mirrors each generator.
|
|
226
|
+
* Row-set baselines that are not per-kind envelopes. `keyOrder` mirrors each
|
|
227
|
+
* generator.
|
|
281
228
|
*/
|
|
282
229
|
const PLAIN_BASELINE_KINDS = Object.freeze({
|
|
283
230
|
cyclomatic: Object.freeze({
|
|
284
231
|
rowIdentity: (row) => String(row?.file ?? ''),
|
|
285
232
|
sortRows: (rows) =>
|
|
286
233
|
[...rows].sort((a, b) => String(a.file).localeCompare(String(b.file))),
|
|
287
|
-
|
|
288
|
-
keyOrder: ['$schema', 'generatedAt', 'ceiling', 'rollup', 'rows'],
|
|
234
|
+
keyOrder: ['$schema', 'ceiling', 'rows'],
|
|
289
235
|
}),
|
|
290
236
|
'dead-exports': Object.freeze({
|
|
291
237
|
rowIdentity: (row) => `${row?.file ?? ''}::${row?.symbol ?? ''}`,
|
|
@@ -295,8 +241,7 @@ const PLAIN_BASELINE_KINDS = Object.freeze({
|
|
|
295
241
|
String(a.file).localeCompare(String(b.file)) ||
|
|
296
242
|
String(a.symbol).localeCompare(String(b.symbol)),
|
|
297
243
|
),
|
|
298
|
-
|
|
299
|
-
keyOrder: ['$schema', 'kernelVersion', 'generatedAt', 'mode', 'rows'],
|
|
244
|
+
keyOrder: ['$schema', 'kernelVersion', 'mode', 'rows'],
|
|
300
245
|
}),
|
|
301
246
|
});
|
|
302
247
|
|
|
@@ -357,15 +302,7 @@ export function mergePlainBaseline({ base, ours, theirs, kind } = {}) {
|
|
|
357
302
|
ours ?? {},
|
|
358
303
|
theirs ?? {},
|
|
359
304
|
);
|
|
360
|
-
const envelope = orderKeys(
|
|
361
|
-
{
|
|
362
|
-
...stamps,
|
|
363
|
-
generatedAt: laterStamp(ours?.generatedAt, theirs?.generatedAt),
|
|
364
|
-
...(spec.rollup ? { rollup: spec.rollup(sortedRows) } : {}),
|
|
365
|
-
rows: sortedRows,
|
|
366
|
-
},
|
|
367
|
-
spec.keyOrder,
|
|
368
|
-
);
|
|
305
|
+
const envelope = orderKeys({ ...stamps, rows: sortedRows }, spec.keyOrder);
|
|
369
306
|
return {
|
|
370
307
|
kind: resolvedKind,
|
|
371
308
|
envelope,
|
|
@@ -380,8 +317,8 @@ const REGENERATE_COMMANDS = Object.freeze({
|
|
|
380
317
|
});
|
|
381
318
|
|
|
382
319
|
/**
|
|
383
|
-
* After a conflicted merge, hand-
|
|
384
|
-
* the driver must name the regeneration command.
|
|
320
|
+
* After a conflicted merge, hand-resolved rows describe a tree nobody scored,
|
|
321
|
+
* so the driver must name the regeneration command.
|
|
385
322
|
*
|
|
386
323
|
* @param {string} kind
|
|
387
324
|
* @returns {string}
|
|
@@ -130,21 +130,21 @@ function validate(kind, parsed, sourceHint) {
|
|
|
130
130
|
/**
|
|
131
131
|
* The one narrowing projection for every loaded envelope. It is an
|
|
132
132
|
* allow-list: compat axes read stamps off the loaded object and fail closed
|
|
133
|
-
* on `undefined`, so every envelope stamp must be carried through here.
|
|
133
|
+
* on `undefined`, so every envelope stamp must be carried through here. The
|
|
134
|
+
* committed file carries no rollup; a consumer that needs one derives it from
|
|
135
|
+
* `rows` through the kind module.
|
|
134
136
|
*
|
|
135
137
|
* @param {string} kind
|
|
136
138
|
* @param {object} parsed A validated baseline envelope.
|
|
137
|
-
* @returns {{
|
|
139
|
+
* @returns {{ rows: Array<object>, kernelVersion: string }}
|
|
138
140
|
*/
|
|
139
141
|
function shapeEnvelope(kind, parsed) {
|
|
140
142
|
const rows = Array.isArray(parsed.rows)
|
|
141
143
|
? parsed.rows.map((row) => canonicaliseRow(kind, row))
|
|
142
144
|
: [];
|
|
143
145
|
return {
|
|
144
|
-
rollup: parsed.rollup ?? { '*': {} },
|
|
145
146
|
rows,
|
|
146
147
|
kernelVersion: parsed.kernelVersion,
|
|
147
|
-
generatedAt: parsed.generatedAt,
|
|
148
148
|
scoringSemantics: parsed.scoringSemantics,
|
|
149
149
|
tsTranspilerVersion: parsed.tsTranspilerVersion,
|
|
150
150
|
provenanceStamped: parsed.provenanceStamped,
|
|
@@ -154,7 +154,7 @@ function shapeEnvelope(kind, parsed) {
|
|
|
154
154
|
/**
|
|
155
155
|
* @param {string} kind
|
|
156
156
|
* @param {string} absolutePath
|
|
157
|
-
* @returns {{
|
|
157
|
+
* @returns {{ rows: Array<object>, kernelVersion: string }}
|
|
158
158
|
*/
|
|
159
159
|
function readAndShape(kind, absolutePath) {
|
|
160
160
|
let raw;
|
|
@@ -195,7 +195,7 @@ function inferKindFromSchema(schemaValue) {
|
|
|
195
195
|
/**
|
|
196
196
|
* @param {string} kind
|
|
197
197
|
* @param {{ configPath?: string, cwd?: string }} [opts]
|
|
198
|
-
* @returns {{
|
|
198
|
+
* @returns {{ rows: Array<object>, kernelVersion: string }}
|
|
199
199
|
*/
|
|
200
200
|
export function load(kind, opts = {}) {
|
|
201
201
|
if (!Object.hasOwn(KIND_TO_SCHEMA_FILE, kind)) {
|
|
@@ -214,7 +214,7 @@ export function load(kind, opts = {}) {
|
|
|
214
214
|
*
|
|
215
215
|
* @param {string} absolutePath
|
|
216
216
|
* @param {{ kind?: string }} [opts]
|
|
217
|
-
* @returns {{
|
|
217
|
+
* @returns {{ rows: Array<object>, kernelVersion: string }}
|
|
218
218
|
*/
|
|
219
219
|
export function loadFile(absolutePath, opts = {}) {
|
|
220
220
|
if (typeof absolutePath !== 'string' || absolutePath.length === 0) {
|
|
@@ -260,7 +260,6 @@ export function resolveDefaultScorer(kind, { cwd } = {}) {
|
|
|
260
260
|
* fs?: typeof nodeFs,
|
|
261
261
|
* gitDiff?: (args: { baseRef: string, headRef: string, cwd: string }) => Iterable<string> | Promise<Iterable<string>>,
|
|
262
262
|
* cwd?: string,
|
|
263
|
-
* generatedAt?: string,
|
|
264
263
|
* requireRowsForScopeFiles?: boolean,
|
|
265
264
|
* requiredScopeFilePredicate?: (file: string) => boolean,
|
|
266
265
|
* }} opts
|
|
@@ -285,7 +284,6 @@ export async function refreshBaseline(opts = {}) {
|
|
|
285
284
|
fs = nodeFs,
|
|
286
285
|
gitDiff = defaultGitDiff,
|
|
287
286
|
cwd = process.cwd(),
|
|
288
|
-
generatedAt,
|
|
289
287
|
requireRowsForScopeFiles = false,
|
|
290
288
|
requiredScopeFilePredicate,
|
|
291
289
|
} = opts;
|
|
@@ -349,11 +347,10 @@ export async function refreshBaseline(opts = {}) {
|
|
|
349
347
|
scope.mode === 'full'
|
|
350
348
|
? null
|
|
351
349
|
: { mode: 'diff', files: new Set(scope.files) },
|
|
352
|
-
generatedAt: generatedAt ?? process.env.MANDREL_BASELINE_GENERATED_AT,
|
|
353
350
|
});
|
|
354
351
|
|
|
355
352
|
// The writer returns the prior object itself when nothing changed; skip the
|
|
356
|
-
// write so on-disk bytes
|
|
353
|
+
// write so on-disk bytes stay verbatim.
|
|
357
354
|
let wrote = false;
|
|
358
355
|
if (priorEnvelope === null || envelope !== priorEnvelope) {
|
|
359
356
|
writeEnvelopeFile(writePath, envelope, { fsImpl: fs });
|
|
@@ -523,9 +520,7 @@ function readPriorEnvelope(writePath, fs) {
|
|
|
523
520
|
parsed &&
|
|
524
521
|
typeof parsed === 'object' &&
|
|
525
522
|
!Array.isArray(parsed) &&
|
|
526
|
-
Array.isArray(parsed.rows)
|
|
527
|
-
parsed.rollup &&
|
|
528
|
-
typeof parsed.rollup === 'object'
|
|
523
|
+
Array.isArray(parsed.rows)
|
|
529
524
|
) {
|
|
530
525
|
return parsed;
|
|
531
526
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared baseline writer, the single funnel for envelope assembly
|
|
3
|
-
* (`write`: project, canonical-assert, merge, stabilise, sort,
|
|
4
|
-
* validate) and serialisation (`writeFile`: atomic tmp + rename).
|
|
3
|
+
* (`write`: project, canonical-assert, merge, stabilise, sort, stamp, validate) and serialisation (`writeFile`: atomic tmp + rename).
|
|
5
4
|
*
|
|
6
5
|
* @module lib/baselines/writer
|
|
7
6
|
*/
|
|
@@ -16,16 +15,14 @@ import { assertCanonical } from './path-canon.js';
|
|
|
16
15
|
/**
|
|
17
16
|
* Assemble and validate an envelope (no I/O). With `scope`, out-of-scope
|
|
18
17
|
* `prior` rows are preserved first; with `epsilon`, sub-epsilon deltas then
|
|
19
|
-
* resolve to prior bytes. `prior` rows must already be canonical. When
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* resolve to prior bytes. `prior` rows must already be canonical. When the
|
|
19
|
+
* rows deep-equal the prior envelope's, the prior is returned unchanged so a
|
|
20
|
+
* no-op refresh leaves no diff.
|
|
22
21
|
*
|
|
23
22
|
* @param {{
|
|
24
23
|
* kind: string,
|
|
25
24
|
* rows: Array<object>,
|
|
26
|
-
* components?: Array<object>,
|
|
27
25
|
* kernelVersion?: string,
|
|
28
|
-
* generatedAt?: string,
|
|
29
26
|
* prior?: Array<object>,
|
|
30
27
|
* priorEnvelope?: object,
|
|
31
28
|
* epsilon?: number,
|
|
@@ -36,9 +33,7 @@ import { assertCanonical } from './path-canon.js';
|
|
|
36
33
|
export function write({
|
|
37
34
|
kind,
|
|
38
35
|
rows,
|
|
39
|
-
components,
|
|
40
36
|
kernelVersion,
|
|
41
|
-
generatedAt,
|
|
42
37
|
prior,
|
|
43
38
|
priorEnvelope,
|
|
44
39
|
epsilon,
|
|
@@ -79,20 +74,9 @@ export function write({
|
|
|
79
74
|
const stabilised = stabiliseRows(mod, merged, prior, epsilon);
|
|
80
75
|
|
|
81
76
|
const sortedRows = mod.sortRows(stabilised);
|
|
82
|
-
const rollup = mod.rollup(sortedRows, components ?? []);
|
|
83
|
-
|
|
84
|
-
if (!Object.hasOwn(rollup, '*')) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`writer.write: ${kind} rollup is missing the required "*" key`,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
77
|
|
|
90
78
|
const priorEnv = resolvePriorEnvelope(priorEnvelope, prior);
|
|
91
|
-
if (
|
|
92
|
-
priorEnv &&
|
|
93
|
-
deepEqual(sortedRows, priorEnv.rows) &&
|
|
94
|
-
deepEqual(rollup, priorEnv.rollup)
|
|
95
|
-
) {
|
|
79
|
+
if (priorEnv && deepEqual(sortedRows, priorEnv.rows)) {
|
|
96
80
|
// No smuggling an invalid envelope through the short-circuit.
|
|
97
81
|
assertEnvelope(priorEnv);
|
|
98
82
|
return priorEnv;
|
|
@@ -101,9 +85,7 @@ export function write({
|
|
|
101
85
|
const envelope = buildEnvelope({
|
|
102
86
|
kind,
|
|
103
87
|
rows: sortedRows,
|
|
104
|
-
rollup,
|
|
105
88
|
kernelVersion: kernelVersion ?? currentKernelVersion(kind),
|
|
106
|
-
generatedAt,
|
|
107
89
|
extras:
|
|
108
90
|
typeof mod.envelopeExtras === 'function' ? mod.envelopeExtras() : null,
|
|
109
91
|
});
|
|
@@ -113,29 +95,20 @@ export function write({
|
|
|
113
95
|
|
|
114
96
|
/** `priorEnvelope`, else `prior` when it is a whole envelope, else null. */
|
|
115
97
|
function resolvePriorEnvelope(priorEnvelope, prior) {
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
typeof priorEnvelope === 'object' &&
|
|
119
|
-
!Array.isArray(priorEnvelope) &&
|
|
120
|
-
Array.isArray(priorEnvelope.rows) &&
|
|
121
|
-
priorEnvelope.rollup &&
|
|
122
|
-
typeof priorEnvelope.rollup === 'object'
|
|
123
|
-
) {
|
|
124
|
-
return priorEnvelope;
|
|
125
|
-
}
|
|
126
|
-
if (
|
|
127
|
-
prior &&
|
|
128
|
-
typeof prior === 'object' &&
|
|
129
|
-
!Array.isArray(prior) &&
|
|
130
|
-
Array.isArray(prior.rows) &&
|
|
131
|
-
prior.rollup &&
|
|
132
|
-
typeof prior.rollup === 'object'
|
|
133
|
-
) {
|
|
134
|
-
return prior;
|
|
135
|
-
}
|
|
98
|
+
if (isEnvelopeObject(priorEnvelope)) return priorEnvelope;
|
|
99
|
+
if (isEnvelopeObject(prior)) return prior;
|
|
136
100
|
return null;
|
|
137
101
|
}
|
|
138
102
|
|
|
103
|
+
function isEnvelopeObject(value) {
|
|
104
|
+
return (
|
|
105
|
+
Boolean(value) &&
|
|
106
|
+
typeof value === 'object' &&
|
|
107
|
+
!Array.isArray(value) &&
|
|
108
|
+
Array.isArray(value.rows)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
139
112
|
/**
|
|
140
113
|
* Serialise deterministically (fixed top-level key order, two-space indent,
|
|
141
114
|
* trailing newline) and atomically (tmp + rename).
|
|
@@ -161,11 +134,9 @@ export function writeFile(absPath, envelope, opts = {}) {
|
|
|
161
134
|
const canonical = {
|
|
162
135
|
$schema: envelope.$schema,
|
|
163
136
|
kernelVersion: envelope.kernelVersion,
|
|
164
|
-
generatedAt: envelope.generatedAt,
|
|
165
137
|
scoringSemantics: envelope.scoringSemantics,
|
|
166
138
|
tsTranspilerVersion: envelope.tsTranspilerVersion,
|
|
167
139
|
provenanceStamped: envelope.provenanceStamped,
|
|
168
|
-
rollup: envelope.rollup,
|
|
169
140
|
rows: envelope.rows,
|
|
170
141
|
};
|
|
171
142
|
|
|
@@ -156,8 +156,6 @@ function writeEnvelopeViaFsImpl(abs, envelope, fsImpl) {
|
|
|
156
156
|
const canonical = {
|
|
157
157
|
$schema: envelope.$schema,
|
|
158
158
|
kernelVersion: envelope.kernelVersion,
|
|
159
|
-
generatedAt: envelope.generatedAt,
|
|
160
|
-
rollup: envelope.rollup,
|
|
161
159
|
rows: envelope.rows,
|
|
162
160
|
};
|
|
163
161
|
fsImpl.writeFileSync(abs, `${JSON.stringify(canonical, null, 2)}\n`);
|
|
@@ -163,29 +163,17 @@ export function diffCyclomaticRows(baselineRows, currentRows) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
*
|
|
166
|
+
* No stamp and no rollup: both would rewrite on every refresh and conflict
|
|
167
|
+
* textually wherever the merge driver does not run.
|
|
168
|
+
*
|
|
169
|
+
* @param {{ rows: Array<object>, ceiling: number }} args
|
|
167
170
|
* @returns {object}
|
|
168
171
|
*/
|
|
169
|
-
export function buildCyclomaticEnvelope({ rows, ceiling
|
|
170
|
-
const safeRows = rows ?? [];
|
|
171
|
-
let methods = 0;
|
|
172
|
-
let max = 0;
|
|
173
|
-
for (const row of safeRows) {
|
|
174
|
-
methods += Number(row.methodsAboveCeiling ?? 0);
|
|
175
|
-
if (Number(row.maxCyclomatic ?? 0) > max) max = Number(row.maxCyclomatic);
|
|
176
|
-
}
|
|
172
|
+
export function buildCyclomaticEnvelope({ rows, ceiling }) {
|
|
177
173
|
return {
|
|
178
174
|
$schema: CYCLOMATIC_BASELINE_SCHEMA,
|
|
179
|
-
generatedAt: generatedAt ?? new Date().toISOString(),
|
|
180
175
|
ceiling,
|
|
181
|
-
|
|
182
|
-
'*': {
|
|
183
|
-
filesAboveCeiling: safeRows.length,
|
|
184
|
-
methodsAboveCeiling: methods,
|
|
185
|
-
maxCyclomatic: max,
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
rows: safeRows,
|
|
176
|
+
rows: rows ?? [],
|
|
189
177
|
};
|
|
190
178
|
}
|
|
191
179
|
|
|
@@ -38,7 +38,7 @@ function refuseWithoutMergeDriver(cwd, gitSpawn) {
|
|
|
38
38
|
`.gitattributes routes baselines/*.json through the mandrel-baseline merge ` +
|
|
39
39
|
`driver, but ${BASELINE_MERGE_DRIVER_CONFIG_KEY} is unset in this clone. ` +
|
|
40
40
|
'Merging origin now would text-merge generated baselines, which conflicts on ' +
|
|
41
|
-
'
|
|
41
|
+
'adjacent rows and can splice rows neither branch scored. Register the ' +
|
|
42
42
|
`driver, then re-run:\n ${BASELINE_MERGE_DRIVER_REMEDY}\n` +
|
|
43
43
|
' (or: npm run baselines:merge-driver)',
|
|
44
44
|
remedy: BASELINE_MERGE_DRIVER_REMEDY,
|
|
@@ -17,34 +17,28 @@ import { applyFloors, flattenBreaches } from './floors.js';
|
|
|
17
17
|
import { applyRefreshAcknowledgment } from './refresh-ack.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* drops them
|
|
23
|
-
*
|
|
20
|
+
* The floor rollup, derived from the rows (the committed file carries none).
|
|
21
|
+
* `ignoreGlobs` rows are dropped first in case a baseline was poisoned by some
|
|
22
|
+
* route other than generation, which already drops them; an ignored file
|
|
23
|
+
* could otherwise breach a global floor.
|
|
24
24
|
*
|
|
25
|
-
* @param {{ kind: string, baseline: {
|
|
25
|
+
* @param {{ kind: string, baseline: { rows?: object[] }, ignoreGlobs?: string[], cwd?: string }} args
|
|
26
26
|
* @returns {object}
|
|
27
27
|
*/
|
|
28
|
-
function
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const p = row?.path;
|
|
43
|
-
return typeof p !== 'string' || !isIgnoredByGlobs(p, ignoreGlobs, cwd);
|
|
44
|
-
});
|
|
45
|
-
if (kept.length === rows.length) return rollup;
|
|
46
|
-
const recomputed = mod.rollup(kept);
|
|
47
|
-
return { ...rollup, '*': recomputed?.['*'] ?? rollup?.['*'] };
|
|
28
|
+
function floorRollupOf({ kind, baseline, ignoreGlobs, cwd }) {
|
|
29
|
+
const mod = getKindModule(kind);
|
|
30
|
+
const rows = Array.isArray(baseline?.rows) ? baseline.rows : [];
|
|
31
|
+
const filterIgnored =
|
|
32
|
+
mod.keyField === 'path' &&
|
|
33
|
+
Array.isArray(ignoreGlobs) &&
|
|
34
|
+
ignoreGlobs.length > 0;
|
|
35
|
+
const kept = filterIgnored
|
|
36
|
+
? rows.filter((row) => {
|
|
37
|
+
const p = row?.path;
|
|
38
|
+
return typeof p !== 'string' || !isIgnoredByGlobs(p, ignoreGlobs, cwd);
|
|
39
|
+
})
|
|
40
|
+
: rows;
|
|
41
|
+
return mod.rollup(kept);
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
function loadHeadBaseline(kind, cwd, configPath) {
|
|
@@ -87,7 +81,6 @@ function buildGateReport({
|
|
|
87
81
|
baseRef: cmp.baseRef ?? null,
|
|
88
82
|
// Without this a compare arm that never ran looks identical to a clean run.
|
|
89
83
|
baseRead: cmp.baseRead === true,
|
|
90
|
-
generatedAt: baseline.generatedAt,
|
|
91
84
|
acknowledged: ack.acknowledged,
|
|
92
85
|
// Acknowledgment can be partial; name which regressions it cleared.
|
|
93
86
|
acknowledgedKeys: ack.acknowledgedKeys,
|
|
@@ -114,7 +107,7 @@ export async function evaluateKind({
|
|
|
114
107
|
schemaError: { tag: 'semantics', message: semanticsError },
|
|
115
108
|
};
|
|
116
109
|
}
|
|
117
|
-
const floorRollup =
|
|
110
|
+
const floorRollup = floorRollupOf({
|
|
118
111
|
kind,
|
|
119
112
|
baseline,
|
|
120
113
|
ignoreGlobs: gateBlock.ignoreGlobs,
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* merge-baseline.js — git merge driver for `baselines/*.json`.
|
|
5
5
|
*
|
|
6
|
-
* A text merge of a baseline
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* A text merge of a baseline conflicts on rows adjacent in sort order or
|
|
7
|
+
* appended at the tail, and can splice both sides' rows into a set no scorer
|
|
8
|
+
* produced. This merges rows by identity instead. The committed shape carries
|
|
9
|
+
* no stamp or rollup, so disjoint non-adjacent refreshes also merge textually
|
|
10
|
+
* where this driver never runs (GitHub). Files that are not a known row
|
|
11
|
+
* baseline go to `git merge-file` unchanged.
|
|
10
12
|
*
|
|
11
13
|
* node .agents/scripts/merge-baseline.js %O %A %B %P
|
|
12
14
|
*
|
|
@@ -115,7 +117,7 @@ function delegateToGit(basePath, oursPath, theirsPath) {
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
/**
|
|
118
|
-
* Envelope kinds
|
|
120
|
+
* Envelope kinds or plain row baselines (cyclomatic,
|
|
119
121
|
* dead-exports*); anything else resolves `null` and goes back to git.
|
|
120
122
|
*
|
|
121
123
|
* @param {unknown} ours
|
|
@@ -269,8 +271,8 @@ function markConflicts({ kind, merged, oursPath, label }) {
|
|
|
269
271
|
}
|
|
270
272
|
|
|
271
273
|
/**
|
|
272
|
-
* Names the regenerate command:
|
|
273
|
-
*
|
|
274
|
+
* Names the regenerate command: hand-resolved rows describe a tree nobody
|
|
275
|
+
* scored.
|
|
274
276
|
*
|
|
275
277
|
* @param {{ kind: string, label: string, rowConflicts: Array<object>, envelopeConflicts: Array<object> }} args
|
|
276
278
|
*/
|
|
@@ -286,9 +288,9 @@ function reportConflicts({ kind, label, rowConflicts, envelopeConflicts }) {
|
|
|
286
288
|
);
|
|
287
289
|
}
|
|
288
290
|
process.stderr.write(
|
|
289
|
-
`merge-baseline: ${label} is conflicted —
|
|
290
|
-
`
|
|
291
|
-
|
|
291
|
+
`merge-baseline: ${label} is conflicted — hand-resolved rows describe a ` +
|
|
292
|
+
`tree nobody scored. After resolving the markers, regenerate it: ` +
|
|
293
|
+
`${baselineRegenerateRemedy(kind)}\n`,
|
|
292
294
|
);
|
|
293
295
|
}
|
|
294
296
|
|
|
@@ -302,7 +304,7 @@ runAsCli(import.meta.url, main, {
|
|
|
302
304
|
usage: {
|
|
303
305
|
invocation: 'node .agents/scripts/merge-baseline.js %O %A %B %P',
|
|
304
306
|
summary:
|
|
305
|
-
'Git merge driver for baselines/*.json. Merges per-kind envelopes by ROW IDENTITY — disjoint refreshes merge clean
|
|
307
|
+
'Git merge driver for baselines/*.json. Merges per-kind envelopes by ROW IDENTITY — disjoint refreshes merge clean even where their rows are adjacent, and a row both sides changed differently conflicts. A baselines file that is not a known per-kind envelope is handed back to git merge-file unchanged. Exit 0 clean, 1 conflicted.',
|
|
306
308
|
flags: [
|
|
307
309
|
['%O', 'Merge ancestor (git supplies this).'],
|
|
308
310
|
['%A', 'Our version — the driver writes its result here.'],
|
|
@@ -114,7 +114,8 @@ Two envelope-level reads come **before** any finding:
|
|
|
114
114
|
gate **High** (delivery reads that file every run), a dead glob **Low**.
|
|
115
115
|
|
|
116
116
|
2. **Staleness.** Two clocks. `staleDays` is whole days since the baseline's
|
|
117
|
-
own
|
|
117
|
+
own stamp where it carries one, else its last-commit date (row-set
|
|
118
|
+
baselines carry no stamp); `staleCommits` is commits touching the measured surface
|
|
118
119
|
since the baseline was last committed, with `surfaceStale` its boolean. A
|
|
119
120
|
`null` on either is never a fabricated zero — the stamp is unreadable, git
|
|
120
121
|
cannot answer, or the rows are not file paths — and is itself the finding.
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,17 @@ All notable changes to this project will be documented in this file.
|
|
|
15
15
|
-->
|
|
16
16
|
<!-- markdownlint-disable-file MD004 MD012 MD037 -->
|
|
17
17
|
|
|
18
|
+
## [2.63.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.62.0...mandrel-v2.63.0) (2026-09-18)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### ⚠ BREAKING CHANGES
|
|
22
|
+
|
|
23
|
+
* committed baselines/*.json row-set files no longer carry `generatedAt` or `rollup`, and the per-kind schemas reject both keys. Run `mandrel migrate` (step 2.63.0, run by `mandrel update`) to rewrite existing baselines. `MANDREL_BASELINE_GENERATED_AT` is retired.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
* make committed row-set baselines merge clean without the custom merge driver ([#5400](https://github.com/dsj1984/mandrel/issues/5400)) ([#5401](https://github.com/dsj1984/mandrel/issues/5401)) ([8a2df0a](https://github.com/dsj1984/mandrel/commit/8a2df0aeb6c75c2c08ef70eeb970b88bef716bde))
|
|
28
|
+
|
|
18
29
|
## [2.62.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.61.0...mandrel-v2.62.0) (2026-09-18)
|
|
19
30
|
|
|
20
31
|
|
package/lib/cli/registry.js
CHANGED
|
@@ -814,7 +814,7 @@ export function runMergeDriver({ cwd, fsImpl = fs, runner = spawn } = {}) {
|
|
|
814
814
|
if (command === '') {
|
|
815
815
|
return {
|
|
816
816
|
ok: false,
|
|
817
|
-
detail: `${BASELINE_MERGE_DRIVER_CONFIG_KEY} is unset — baselines/*.json will fall back to git's text merge, which conflicts on
|
|
817
|
+
detail: `${BASELINE_MERGE_DRIVER_CONFIG_KEY} is unset — baselines/*.json will fall back to git's text merge, which conflicts on adjacent rows and can splice rows neither branch scored`,
|
|
818
818
|
remedy: BASELINE_MERGE_DRIVER_REMEDY,
|
|
819
819
|
};
|
|
820
820
|
}
|
package/lib/migrations/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { retireLintBaselineCommand } from './steps/2.32.0-retire-lint-baseline-c
|
|
|
15
15
|
import { retireDeliveryLimitKnobs } from './steps/2.57.0-retire-delivery-limit-knobs.js';
|
|
16
16
|
import { retirePlanningLimitKnobs } from './steps/2.57.0-retire-planning-limit-knobs.js';
|
|
17
17
|
import { retireAuditResultsAutoFile } from './steps/2.60.0-retire-audit-results-autofile.js';
|
|
18
|
+
import { baselineMergeQueueShape } from './steps/2.63.0-baseline-merge-queue-shape.js';
|
|
18
19
|
import { stripRemovedAgentrcKeys } from './steps/strip-removed-agentrc-keys.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -38,6 +39,7 @@ export const migrations = [
|
|
|
38
39
|
retireDeliveryLimitKnobs,
|
|
39
40
|
retireAuditResultsAutoFile,
|
|
40
41
|
stripRemovedAgentrcKeys,
|
|
42
|
+
baselineMergeQueueShape,
|
|
41
43
|
];
|
|
42
44
|
|
|
43
45
|
export { compareVersions };
|