mandrel 2.61.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.
Files changed (34) hide show
  1. package/.agents/docs/configuration.md +0 -1
  2. package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +1 -1
  3. package/.agents/schemas/baselines/baseline-envelope.schema.json +2 -19
  4. package/.agents/schemas/baselines/bundle-size.schema.json +0 -24
  5. package/.agents/schemas/baselines/coverage.schema.json +0 -26
  6. package/.agents/schemas/baselines/crap.schema.json +0 -28
  7. package/.agents/schemas/baselines/duplication.schema.json +0 -38
  8. package/.agents/schemas/baselines/maintainability.schema.json +0 -45
  9. package/.agents/schemas/baselines/mutation.schema.json +0 -28
  10. package/.agents/scripts/check-dead-exports.js +1 -1
  11. package/.agents/scripts/lib/audit-baselines/headroom.js +1 -1
  12. package/.agents/scripts/lib/audit-baselines/kinds.js +12 -7
  13. package/.agents/scripts/lib/audit-baselines/rollup.js +41 -0
  14. package/.agents/scripts/lib/audit-baselines/staleness.js +14 -3
  15. package/.agents/scripts/lib/audit-baselines/surface-entry.js +1 -1
  16. package/.agents/scripts/lib/baselines/envelope.js +5 -50
  17. package/.agents/scripts/lib/baselines/merge-envelopes.js +16 -79
  18. package/.agents/scripts/lib/baselines/reader.js +7 -7
  19. package/.agents/scripts/lib/baselines/refresh-service.js +2 -7
  20. package/.agents/scripts/lib/baselines/writer.js +16 -45
  21. package/.agents/scripts/lib/coverage-baseline.js +0 -2
  22. package/.agents/scripts/lib/cyclomatic-ceiling.js +6 -18
  23. package/.agents/scripts/lib/git/sync-from-base.js +1 -1
  24. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +20 -27
  25. package/.agents/scripts/lib/orchestration/merge-queue.js +158 -0
  26. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +115 -49
  27. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +49 -1
  28. package/.agents/scripts/merge-baseline.js +13 -11
  29. package/.agents/workflows/audit-baselines.md +2 -1
  30. package/docs/CHANGELOG.md +18 -0
  31. package/lib/cli/registry.js +1 -1
  32. package/lib/migrations/index.js +2 -0
  33. package/lib/migrations/steps/2.63.0-baseline-merge-queue-shape.js +96 -0
  34. package/package.json +1 -1
@@ -593,7 +593,6 @@ Claude Code web environment-variables UI for web sessions.
593
593
  | `WEBHOOK_SECRET` | No | Shared secret used to sign outbound webhook payloads as `X-Signature-256: sha256=<hmac>`. Unset ships unsigned payloads. |
594
594
  | `MANDREL_ALLOW_TEST_WEBHOOKS` | No | Set to `1` to keep `NOTIFICATION_WEBHOOK_URL` live inside `npm test` / `npm run test:profile`. Default behaviour scrubs the env var from the test child so no URL resolves and the webhook never fires (see below). |
595
595
  | `MANDREL_POOL_CONCURRENCY` | No | Upper bound on the width of every `runOnPool` worker pool in the process (the MI and CRAP scan pools). Precedence is: a caller's explicit `concurrency` → this variable → a clamp of 4 under `node:test` → `os.availableParallelism()`. Set it on a constrained or shared runner where one pool per core oversubscribes the host; a non-numeric value is ignored rather than collapsing the pool. |
596
- | `MANDREL_BASELINE_GENERATED_AT` | No | Pins the `generatedAt` stamp every baseline envelope carries, instead of reading the clock. Set it for a reproducible build, or to make a hand-run refresh diff against a known stamp. It changes only the stamp — rows and rollup are unaffected, and a refresh that moves no row still rewrites nothing. Concurrent refreshes no longer need it to avoid conflicting: `baselines/*.json` merges by row identity (see the baseline merge driver in [quality-gates.md](https://github.com/dsj1984/mandrel/blob/main/docs/quality-gates.md)). |
597
596
  | `MANDREL_AGENTRC_VALIDATOR` | No | Set to `dynamic` to compile the `.agentrc.json` AJV validator at runtime instead of loading the committed precompiled one (see below). Costs ~35 ms per process; the escape hatch exists for a hand-edited schema or a host where the generated module will not load. |
598
597
 
599
598
  ### The `.agentrc` validator is precompiled
@@ -100,7 +100,7 @@
100
100
  "generatedAt": { "type": ["string", "null"] },
101
101
  "staleDays": {
102
102
  "type": ["integer", "null"],
103
- "description": "Whole days since generatedAt. Null when the stamp is absent or unparseable — never a fabricated 0. Wall time only: a baseline can read 0 here and still predate the surface it scores, which is what staleCommits catches."
103
+ "description": "Whole days since generatedAt — the baseline's own stamp where it carries one, else its last-commit date (row-set baselines carry no stamp). Null when neither is available or parseable — never a fabricated 0. Wall time only: a baseline can read 0 here and still predate the surface it scores, which is what staleCommits catches."
104
104
  },
105
105
  "staleCommits": {
106
106
  "type": ["integer", "null"],
@@ -2,9 +2,9 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://mandrel/.agents/schemas/baselines/baseline-envelope.schema.json",
4
4
  "title": "BaselineEnvelope",
5
- "description": "Shared envelope contract for every committed Mandrel baseline (lint, coverage, crap, maintainability, mutation, lighthouse, bundle-size). Each per-kind schema extends this envelope via allOf to constrain the row and rollup shape. The envelope itself is intentionally agnostic about row contents so the kernel can evolve without churning the shared contract.",
5
+ "description": "Shared envelope contract for every committed Mandrel baseline (lint, coverage, crap, maintainability, mutation, lighthouse, bundle-size). Each per-kind schema extends this envelope via allOf to constrain the row shape. The committed file carries no run timestamp and no rollup: both were rewritten on every refresh, so two branches refreshing disjoint rows always conflicted textually — and GitHub (PR mergeability, update-branch, the merge queue) never runs the custom merge driver. Readers derive the rollup from rows[]; freshness is the file's git history. The envelope itself is intentionally agnostic about row contents so the kernel can evolve without churning the shared contract.",
6
6
  "type": "object",
7
- "required": ["$schema", "kernelVersion", "generatedAt", "rollup", "rows"],
7
+ "required": ["$schema", "kernelVersion", "rows"],
8
8
  "additionalProperties": false,
9
9
  "properties": {
10
10
  "$schema": {
@@ -16,11 +16,6 @@
16
16
  "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
17
17
  "description": "Semver version of the in-repo kernel that produced this baseline. Bumped when the scoring formula, scan shape, or rollup math changes. Consumers compare against the running kernel and emit drift signals when they mismatch."
18
18
  },
19
- "generatedAt": {
20
- "type": "string",
21
- "format": "date-time",
22
- "description": "ISO-8601 timestamp of the baseline run. Used for stale-baseline detection and audit trails."
23
- },
24
19
  "scoringSemantics": {
25
20
  "type": "string",
26
21
  "description": "Optional per-kind stamp identifying the SCORING SEMANTICS that produced these rows — the dimension kernelVersion cannot express, because a kind's own scoring can change while the upstream package it stamps does not. A baseline whose stamp differs from the running scorer's is incomparable and the gate fails closed with re-baseline guidance rather than comparing across the boundary."
@@ -33,18 +28,6 @@
33
28
  "type": "boolean",
34
29
  "description": "Optional per-kind POSITIVE assertion that the writer which produced these rows recorded each row's coordinate provenance (Story #4901). Only meaningful for kinds whose rows carry a line coordinate that may be transpiled. Absence is the only evidence a baseline written before provenance existed leaves behind — such a baseline claims by omission that every row is an original-source coordinate — so the kind's compat axis fails it closed with re-seed guidance rather than letting the comparator key rows against a coordinate space they are not in."
35
30
  },
36
- "rollup": {
37
- "type": "object",
38
- "description": "Aggregate metrics keyed by component. The '*' key is reserved for the whole-repo rollup and is REQUIRED; additional keys MAY be present, one per declared component.",
39
- "required": ["*"],
40
- "additionalProperties": true,
41
- "properties": {
42
- "*": {
43
- "type": "object",
44
- "description": "Whole-repo rollup. Per-kind schemas tighten the inner shape."
45
- }
46
- }
47
- },
48
31
  "rows": {
49
32
  "type": "array",
50
33
  "description": "Per-row metrics. Order is kind-specific (e.g. CRAP sorts by file+startLine+method); per-kind schemas tighten the item shape.",
@@ -6,30 +6,6 @@
6
6
  "allOf": [{ "$ref": "baseline-envelope.schema.json" }],
7
7
  "type": "object",
8
8
  "properties": {
9
- "rollup": {
10
- "type": "object",
11
- "required": ["*"],
12
- "additionalProperties": {
13
- "type": "object",
14
- "required": ["totalKb", "gzippedKb"],
15
- "additionalProperties": false,
16
- "properties": {
17
- "totalKb": { "type": "number", "minimum": 0 },
18
- "gzippedKb": { "type": "number", "minimum": 0 }
19
- }
20
- },
21
- "properties": {
22
- "*": {
23
- "type": "object",
24
- "required": ["totalKb", "gzippedKb"],
25
- "additionalProperties": false,
26
- "properties": {
27
- "totalKb": { "type": "number", "minimum": 0 },
28
- "gzippedKb": { "type": "number", "minimum": 0 }
29
- }
30
- }
31
- }
32
- },
33
9
  "rows": {
34
10
  "type": "array",
35
11
  "items": {
@@ -6,32 +6,6 @@
6
6
  "allOf": [{ "$ref": "baseline-envelope.schema.json" }],
7
7
  "type": "object",
8
8
  "properties": {
9
- "rollup": {
10
- "type": "object",
11
- "required": ["*"],
12
- "additionalProperties": {
13
- "type": "object",
14
- "required": ["lines", "branches", "functions"],
15
- "additionalProperties": false,
16
- "properties": {
17
- "lines": { "type": "number", "minimum": 0, "maximum": 100 },
18
- "branches": { "type": "number", "minimum": 0, "maximum": 100 },
19
- "functions": { "type": "number", "minimum": 0, "maximum": 100 }
20
- }
21
- },
22
- "properties": {
23
- "*": {
24
- "type": "object",
25
- "required": ["lines", "branches", "functions"],
26
- "additionalProperties": false,
27
- "properties": {
28
- "lines": { "type": "number", "minimum": 0, "maximum": 100 },
29
- "branches": { "type": "number", "minimum": 0, "maximum": 100 },
30
- "functions": { "type": "number", "minimum": 0, "maximum": 100 }
31
- }
32
- }
33
- }
34
- },
35
9
  "rows": {
36
10
  "type": "array",
37
11
  "items": {
@@ -14,34 +14,6 @@
14
14
  "type": "boolean",
15
15
  "description": "Positive assertion that the writer which produced these rows recorded coordinate provenance per row (Story #4901). Absence is the ONLY evidence a pre-Story-#4866 baseline leaves — such a baseline claims by omission that every row is an original-source coordinate while some are transpiled. Deliberately optional and unconstrained here for the same reason as scoringSemantics: an unstamped baseline must reach the `provenance-unstamped` compat axis, which fails it closed with the re-seed command when it also carries transpiled-source rows, rather than dying on a raw schema error. A pure-JavaScript baseline never needs it."
16
16
  },
17
- "rollup": {
18
- "type": "object",
19
- "required": ["*"],
20
- "additionalProperties": {
21
- "type": "object",
22
- "required": ["p50", "p95", "max", "methodsAbove20"],
23
- "additionalProperties": false,
24
- "properties": {
25
- "p50": { "type": "number", "minimum": 0 },
26
- "p95": { "type": "number", "minimum": 0 },
27
- "max": { "type": "number", "minimum": 0 },
28
- "methodsAbove20": { "type": "integer", "minimum": 0 }
29
- }
30
- },
31
- "properties": {
32
- "*": {
33
- "type": "object",
34
- "required": ["p50", "p95", "max", "methodsAbove20"],
35
- "additionalProperties": false,
36
- "properties": {
37
- "p50": { "type": "number", "minimum": 0 },
38
- "p95": { "type": "number", "minimum": 0 },
39
- "max": { "type": "number", "minimum": 0 },
40
- "methodsAbove20": { "type": "integer", "minimum": 0 }
41
- }
42
- }
43
- }
44
- },
45
17
  "rows": {
46
18
  "type": "array",
47
19
  "items": {
@@ -6,44 +6,6 @@
6
6
  "allOf": [{ "$ref": "baseline-envelope.schema.json" }],
7
7
  "type": "object",
8
8
  "properties": {
9
- "rollup": {
10
- "type": "object",
11
- "required": ["*"],
12
- "additionalProperties": {
13
- "type": "object",
14
- "required": [
15
- "percentage",
16
- "duplicatedLines",
17
- "totalLines",
18
- "filesWithDuplication"
19
- ],
20
- "additionalProperties": false,
21
- "properties": {
22
- "percentage": { "type": "number", "minimum": 0, "maximum": 100 },
23
- "duplicatedLines": { "type": "integer", "minimum": 0 },
24
- "totalLines": { "type": "integer", "minimum": 0 },
25
- "filesWithDuplication": { "type": "integer", "minimum": 0 }
26
- }
27
- },
28
- "properties": {
29
- "*": {
30
- "type": "object",
31
- "required": [
32
- "percentage",
33
- "duplicatedLines",
34
- "totalLines",
35
- "filesWithDuplication"
36
- ],
37
- "additionalProperties": false,
38
- "properties": {
39
- "percentage": { "type": "number", "minimum": 0, "maximum": 100 },
40
- "duplicatedLines": { "type": "integer", "minimum": 0 },
41
- "totalLines": { "type": "integer", "minimum": 0 },
42
- "filesWithDuplication": { "type": "integer", "minimum": 0 }
43
- }
44
- }
45
- }
46
- },
47
9
  "rows": {
48
10
  "type": "array",
49
11
  "items": {
@@ -6,51 +6,6 @@
6
6
  "allOf": [{ "$ref": "baseline-envelope.schema.json" }],
7
7
  "type": "object",
8
8
  "properties": {
9
- "rollup": {
10
- "type": "object",
11
- "description": "Per-component rollup keyed by component name. The catch-all `*` is required. Each component exposes `min` / `p50` / `p95` of the per-row `mi` values — the same axes that configured floors compare against in `check-baselines.js`.",
12
- "required": ["*"],
13
- "additionalProperties": {
14
- "type": "object",
15
- "required": ["min", "p50", "p95"],
16
- "additionalProperties": false,
17
- "properties": {
18
- "min": {
19
- "type": "number",
20
- "description": "Lowest `mi` value across the component's rows. Targeted by the framework-default 70-MI floor."
21
- },
22
- "p50": {
23
- "type": "number",
24
- "description": "Median (50th percentile) of the component's `mi` values."
25
- },
26
- "p95": {
27
- "type": "number",
28
- "description": "95th-percentile of the component's `mi` values."
29
- }
30
- }
31
- },
32
- "properties": {
33
- "*": {
34
- "type": "object",
35
- "required": ["min", "p50", "p95"],
36
- "additionalProperties": false,
37
- "properties": {
38
- "min": {
39
- "type": "number",
40
- "description": "Whole-repo minimum `mi`. The framework-default floor `{ min: 70 }` enforces against this value."
41
- },
42
- "p50": {
43
- "type": "number",
44
- "description": "Whole-repo median `mi`."
45
- },
46
- "p95": {
47
- "type": "number",
48
- "description": "Whole-repo 95th-percentile `mi`."
49
- }
50
- }
51
- }
52
- }
53
- },
54
9
  "rows": {
55
10
  "type": "array",
56
11
  "description": "Per-file rows. The row axis stays `mi` (unchanged by Story #2193 — only the documented floor axis moved to the rollup `min` key).",
@@ -6,34 +6,6 @@
6
6
  "allOf": [{ "$ref": "baseline-envelope.schema.json" }],
7
7
  "type": "object",
8
8
  "properties": {
9
- "rollup": {
10
- "type": "object",
11
- "required": ["*"],
12
- "additionalProperties": {
13
- "type": "object",
14
- "required": ["score", "killed", "survived", "noCoverage"],
15
- "additionalProperties": false,
16
- "properties": {
17
- "score": { "type": "number", "minimum": 0, "maximum": 100 },
18
- "killed": { "type": "integer", "minimum": 0 },
19
- "survived": { "type": "integer", "minimum": 0 },
20
- "noCoverage": { "type": "integer", "minimum": 0 }
21
- }
22
- },
23
- "properties": {
24
- "*": {
25
- "type": "object",
26
- "required": ["score", "killed", "survived", "noCoverage"],
27
- "additionalProperties": false,
28
- "properties": {
29
- "score": { "type": "number", "minimum": 0, "maximum": 100 },
30
- "killed": { "type": "integer", "minimum": 0 },
31
- "survived": { "type": "integer", "minimum": 0 },
32
- "noCoverage": { "type": "integer", "minimum": 0 }
33
- }
34
- }
35
- }
36
- },
37
9
  "rows": {
38
10
  "type": "array",
39
11
  "items": {
@@ -59,7 +59,7 @@ export function parseArgv(argv = []) {
59
59
  * `null` when missing or unparseable.
60
60
  *
61
61
  * @param {string} baselinePath
62
- * @returns {{ kernelVersion?: string, generatedAt?: string, rows?: Array<{file: string, symbol: string}> } | null}
62
+ * @returns {{ kernelVersion?: string, rows?: Array<{file: string, symbol: string}> } | null}
63
63
  */
64
64
  export function loadBaseline(baselinePath) {
65
65
  try {
@@ -35,7 +35,7 @@ export function buildHeadroom({ kinds, quality, baselines }) {
35
35
  for (const kind of kinds) {
36
36
  const floors = quality?.gates?.[kind]?.floors?.['*'];
37
37
  if (!floors || typeof floors !== 'object') continue;
38
- const rollup = rollupOf(baselines.get(kind) ?? null);
38
+ const rollup = rollupOf(kind, baselines.get(kind) ?? null);
39
39
  for (const [axis, floor] of Object.entries(floors)) {
40
40
  if (typeof floor !== 'number' || !Number.isFinite(floor)) continue;
41
41
  const measured = rollup?.[axis];
@@ -5,7 +5,10 @@
5
5
  * @module lib/audit-baselines/kinds
6
6
  */
7
7
 
8
+ import { KNOWN_KINDS } from '../baselines/envelope.js';
9
+ import { getKindModule } from '../baselines/kernel.js';
8
10
  import { GATES_SCHEMA } from '../config/gates/index.js';
11
+ import { rollupOfRows } from './rollup.js';
9
12
 
10
13
  /** Derived from the schema so a new gate kind arrives automatically. */
11
14
  export const GATE_KINDS = Object.freeze(
@@ -203,15 +206,17 @@ export const KIND_SPECS = Object.freeze({
203
206
  });
204
207
 
205
208
  /**
206
- * The `*` rollup, or `null` (ratchets carry none — a zero-cycle `arch-cycles`
207
- * is a passing gate, not a stub).
209
+ * The `*` rollup derived from the rows (the committed file carries none), or
210
+ * `null` for a kind without rollup arithmetic — a zero-cycle `arch-cycles` is
211
+ * a passing gate, not a stub.
208
212
  *
213
+ * @param {string} kind
209
214
  * @param {object | null} baseline
210
215
  * @returns {object | null}
211
216
  */
212
- export function rollupOf(baseline) {
213
- const rollup = baseline?.rollup?.['*'];
214
- return rollup && typeof rollup === 'object' ? rollup : null;
217
+ export function rollupOf(kind, baseline) {
218
+ const rows = baseline?.rows;
219
+ return Array.isArray(rows) ? rollupOfRows(kind, rows) : null;
215
220
  }
216
221
 
217
222
  /**
@@ -231,7 +236,7 @@ export function measuredTotalOf(kind, baseline) {
231
236
  }
232
237
 
233
238
  /**
234
- * Trend comparison rollup: the declared one, else (ratchets) the measured
239
+ * Trend comparison rollup: the derived one, else (ratchets) the measured
235
240
  * total under its unit, so ratchets appear in `trend[]` too.
236
241
  *
237
242
  * @param {string} kind
@@ -240,7 +245,7 @@ export function measuredTotalOf(kind, baseline) {
240
245
  */
241
246
  export function trendRollupOf(kind, baseline) {
242
247
  if (!baseline) return null;
243
- const declared = rollupOf(baseline);
248
+ const declared = rollupOf(kind, baseline);
244
249
  if (declared) return declared;
245
250
  const measured = measuredTotalOf(kind, baseline);
246
251
  return measured ? { [measured.unit]: measured.value } : null;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The `*` rollup a baseline's rows imply. Committed row-set baselines carry
3
+ * none: a rollup rewritten on every refresh conflicted every concurrent merge.
4
+ *
5
+ * @module lib/audit-baselines/rollup
6
+ */
7
+
8
+ import { KNOWN_KINDS } from '../baselines/envelope.js';
9
+ import { getKindModule } from '../baselines/kernel.js';
10
+
11
+ /**
12
+ * @param {Array<object>} rows
13
+ * @returns {{ filesAboveCeiling: number, methodsAboveCeiling: number, maxCyclomatic: number }}
14
+ */
15
+ function cyclomaticRollup(rows) {
16
+ let methods = 0;
17
+ let max = 0;
18
+ for (const row of rows) {
19
+ methods += Number(row?.methodsAboveCeiling ?? 0);
20
+ const rowMax = Number(row?.maxCyclomatic ?? 0);
21
+ if (rowMax > max) max = rowMax;
22
+ }
23
+ return {
24
+ filesAboveCeiling: rows.length,
25
+ methodsAboveCeiling: methods,
26
+ maxCyclomatic: max,
27
+ };
28
+ }
29
+
30
+ /**
31
+ * `null` for a kind without rollup arithmetic.
32
+ *
33
+ * @param {string} kind
34
+ * @param {Array<object>} rows
35
+ * @returns {object | null}
36
+ */
37
+ export function rollupOfRows(kind, rows) {
38
+ if (KNOWN_KINDS.includes(kind)) return getKindModule(kind).rollup(rows)['*'];
39
+ if (kind === 'cyclomatic') return cyclomaticRollup(rows);
40
+ return null;
41
+ }
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Baseline staleness on two clocks: wall-clock age, and commits touching the
3
3
  * measured surface since the baseline was committed — a baseline behind its
4
- * surface is stale at zero days. Unknown reports `null`, never a reassuring 0.
4
+ * surface is stale at zero days. Age is the file's own stamp where it carries
5
+ * one, else its last-commit date: row-set baselines carry no stamp, since one
6
+ * rewritten on every refresh conflicted every concurrent merge. Unknown
7
+ * reports `null`, never a reassuring 0.
5
8
  *
6
9
  * @module lib/audit-baselines/staleness
7
10
  */
@@ -66,6 +69,15 @@ function commitsSinceBaseline({ relPath, surfacePaths, io }) {
66
69
  return Number.isInteger(commits) ? commits : null;
67
70
  }
68
71
 
72
+ /**
73
+ * @param {{ baseline: object, relPath: string, io: object }} args
74
+ * @returns {string | null}
75
+ */
76
+ function writtenAtOf({ baseline, relPath, io }) {
77
+ if (typeof baseline.generatedAt === 'string') return baseline.generatedAt;
78
+ return git(['log', '-n1', '--format=%cI', '--', relPath], io);
79
+ }
80
+
69
81
  /**
70
82
  * @param {{
71
83
  * kind: string, gateBlock: object | null, rows: Array<{id: string}>,
@@ -86,8 +98,7 @@ export function stalenessOf({
86
98
  now,
87
99
  io,
88
100
  }) {
89
- const generatedAt =
90
- typeof baseline?.generatedAt === 'string' ? baseline.generatedAt : null;
101
+ const generatedAt = baseline ? writtenAtOf({ baseline, relPath, io }) : null;
91
102
  const staleCommits = baseline
92
103
  ? commitsSinceBaseline({
93
104
  relPath,
@@ -68,7 +68,7 @@ export function surfaceEntryFor({
68
68
  const gateBlock = quality?.gates?.[kind] ?? null;
69
69
  const { exists, parsed: baseline, parseError } = read;
70
70
  const rows = baseline ? KIND_SPECS[kind].rows(baseline) : [];
71
- const rollup = rollupOf(baseline);
71
+ const rollup = rollupOf(kind, baseline);
72
72
  return {
73
73
  kind,
74
74
  surface: GATE_KINDS.includes(kind) ? 'gate' : 'ratchet',
@@ -24,56 +24,25 @@ function kernelVersionPattern() {
24
24
  return /^[0-9]+\.[0-9]+\.[0-9]+$/;
25
25
  }
26
26
 
27
- function isoTimestampPattern() {
28
- // Cheap pre-check for a friendlier error; AJV's `date-time` is authoritative.
29
- return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
30
- }
31
-
32
27
  /**
33
- * Explicit value, else `MANDREL_BASELINE_GENERATED_AT` (reproducible builds),
34
- * else now.
28
+ * The committed shape carries neither a run timestamp nor a rollup: both were
29
+ * rewritten on every refresh, so two branches refreshing disjoint rows always
30
+ * conflicted on a platform that never runs the merge driver. Readers derive
31
+ * the rollup from `rows`.
35
32
  *
36
- * @param {string|undefined} explicit
37
- * @returns {string}
38
- */
39
- function resolveGeneratedAt(explicit) {
40
- const candidate =
41
- typeof explicit === 'string' && explicit.length > 0
42
- ? explicit
43
- : (process.env.MANDREL_BASELINE_GENERATED_AT ?? new Date().toISOString());
44
- if (typeof candidate !== 'string' || !isoTimestampPattern().test(candidate)) {
45
- throw new Error(
46
- `envelope.buildEnvelope: generatedAt must be an ISO-8601 timestamp (got ${JSON.stringify(candidate)})`,
47
- );
48
- }
49
- return candidate;
50
- }
51
-
52
- /**
53
33
  * @param {{
54
34
  * kind: string,
55
- * rollup: Record<string, object>,
56
35
  * rows: Array<object>,
57
36
  * kernelVersion: string,
58
- * generatedAt?: string,
59
37
  * extras?: Record<string, unknown>,
60
38
  * }} params
61
39
  * @returns {{
62
40
  * $schema: string,
63
41
  * kernelVersion: string,
64
- * generatedAt: string,
65
- * rollup: Record<string, object>,
66
42
  * rows: Array<object>,
67
43
  * }}
68
44
  */
69
- export function buildEnvelope({
70
- kind,
71
- rollup,
72
- rows,
73
- kernelVersion,
74
- generatedAt,
75
- extras,
76
- } = {}) {
45
+ export function buildEnvelope({ kind, rows, kernelVersion, extras } = {}) {
77
46
  if (typeof kind !== 'string' || !KNOWN_KINDS.includes(kind)) {
78
47
  throw new TypeError(
79
48
  `envelope.buildEnvelope: kind must be one of ${KNOWN_KINDS.join(', ')} (got ${JSON.stringify(kind)})`,
@@ -87,16 +56,6 @@ export function buildEnvelope({
87
56
  `envelope.buildEnvelope: kernelVersion must be semver-shaped (got ${JSON.stringify(kernelVersion)})`,
88
57
  );
89
58
  }
90
- if (!rollup || typeof rollup !== 'object' || Array.isArray(rollup)) {
91
- throw new TypeError(
92
- 'envelope.buildEnvelope: rollup must be an object keyed by component',
93
- );
94
- }
95
- if (!Object.hasOwn(rollup, '*')) {
96
- throw new Error(
97
- 'envelope.buildEnvelope: rollup["*"] (whole-repo rollup) is required',
98
- );
99
- }
100
59
  if (!Array.isArray(rows)) {
101
60
  throw new TypeError('envelope.buildEnvelope: rows must be an array');
102
61
  }
@@ -106,9 +65,7 @@ export function buildEnvelope({
106
65
  return {
107
66
  $schema: schemaRefFor(kind),
108
67
  kernelVersion,
109
- generatedAt: resolveGeneratedAt(generatedAt),
110
68
  ...(extras && typeof extras === 'object' ? extras : {}),
111
- rollup,
112
69
  rows,
113
70
  };
114
71
  }
@@ -137,8 +94,6 @@ function getValidator(kind) {
137
94
  const REQUIRED_TOP_LEVEL_KEYS = Object.freeze([
138
95
  '$schema',
139
96
  'kernelVersion',
140
- 'generatedAt',
141
- 'rollup',
142
97
  'rows',
143
98
  ]);
144
99