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.
- 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/lib/orchestration/merge-queue.js +158 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +115 -49
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +49 -1
- package/.agents/scripts/merge-baseline.js +13 -11
- package/.agents/workflows/audit-baselines.md +2 -1
- package/docs/CHANGELOG.md +18 -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
|
@@ -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
|
|
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
|
|
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", "
|
|
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,
|
|
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
|
|
207
|
-
*
|
|
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
|
|
214
|
-
return
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
34
|
-
*
|
|
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
|
|