mandrel 1.58.0 → 1.59.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/README.md +89 -87
- package/.agents/docs/SDLC.md +11 -7
- package/.agents/docs/workflows.md +2 -1
- package/.agents/schemas/audit-rules.json +20 -0
- package/.agents/scripts/acceptance-eval.js +20 -3
- package/.agents/scripts/assert-branch.js +1 -3
- package/.agents/scripts/bootstrap.js +1 -1
- package/.agents/scripts/check-arch-cycles.js +360 -0
- package/.agents/scripts/coverage-capture.js +24 -3
- package/.agents/scripts/epic-deliver-preflight.js +5 -3
- package/.agents/scripts/epic-deliver-prepare.js +12 -4
- package/.agents/scripts/epic-execute-record-wave.js +1 -1
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/git-rebase-and-resolve.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +34 -14
- package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
- package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
- package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
- package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
- package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
- package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
- package/.agents/scripts/lib/baselines/writer.js +1 -1
- package/.agents/scripts/lib/close-validation/commands.js +188 -0
- package/.agents/scripts/lib/close-validation/gates.js +235 -0
- package/.agents/scripts/lib/close-validation/process.js +101 -0
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
- package/.agents/scripts/lib/close-validation/runner.js +325 -0
- package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
- package/.agents/scripts/lib/config/quality.js +6 -6
- package/.agents/scripts/lib/config-resolver.js +2 -5
- package/.agents/scripts/lib/coverage-capture.js +147 -4
- package/.agents/scripts/lib/cpu-pool.js +14 -0
- package/.agents/scripts/lib/crap-utils.js +6 -11
- package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
- package/.agents/scripts/lib/git-utils.js +24 -22
- package/.agents/scripts/lib/maintainability-engine.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +4 -187
- package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +80 -6
- package/.agents/scripts/lib/orchestration/code-review.js +90 -77
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +5 -12
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +14 -14
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +184 -49
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +26 -2
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +26 -6
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +7 -20
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -6
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -16
- package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -3
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +7 -7
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
- package/.agents/scripts/lib/orchestration/preflight-cache.js +35 -12
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
- package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
- package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
- package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +30 -3
- package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
- package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -33
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +42 -64
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +9 -0
- package/.agents/scripts/lib/orchestration/ticketing/state.js +50 -436
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +1 -1
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -7
- package/.agents/scripts/lib/project-root.js +17 -0
- package/.agents/scripts/lib/story-adjacency.js +76 -0
- package/.agents/scripts/lib/story-lifecycle.js +1 -1
- package/.agents/scripts/lib/transpile.js +93 -0
- package/.agents/scripts/lib/wave-runner/tick.js +4 -153
- package/.agents/scripts/lib/workers/crap-worker.js +1 -1
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
- package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
- package/.agents/scripts/providers/github/tickets.js +110 -6
- package/.agents/scripts/run-lint.js +9 -0
- package/.agents/scripts/run-tests.js +24 -4
- package/.agents/scripts/stories-wave-tick.js +8 -5
- package/.agents/scripts/story-init.js +149 -10
- package/.agents/scripts/sync-branch-from-base.js +1 -1
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
- package/.agents/workflows/audit-documentation.md +226 -0
- package/.agents/workflows/epic-deliver.md +16 -23
- package/.agents/workflows/epic-plan.md +1 -1
- package/.agents/workflows/helpers/epic-deliver-story.md +17 -28
- package/.agents/workflows/helpers/single-story-deliver.md +2 -1
- package/.agents/workflows/onboard.md +4 -3
- package/.agents/workflows/story-deliver.md +1 -1
- package/README.md +13 -8
- package/lib/cli/init.js +336 -0
- package/package.json +2 -1
- package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
- package/.agents/scripts/lib/close-validation.js +0 -897
- package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
- package/.agents/scripts/lib/task-utils.js +0 -26
- package/.agents/scripts/story-deliver-prepare.js +0 -267
|
@@ -7,19 +7,22 @@
|
|
|
7
7
|
* `*` key and each component. Per-row denominators land later when the
|
|
8
8
|
* components resolver and per-component weighting (Story #1902, #1919)
|
|
9
9
|
* arrive; the rollup signature is stable so callers don't churn.
|
|
10
|
+
*
|
|
11
|
+
* Higher percentages are better. New paths land in the `additions` bucket
|
|
12
|
+
* (Story #2012 — partial coverage on a new file must never flip to a
|
|
13
|
+
* regression); removed paths inherit a perfect head row so any lower base
|
|
14
|
+
* registers as an improvement. Scaffold (sortRows / rollup / compare /
|
|
15
|
+
* applyEpsilon / mergeRows) is generated by `makeBaselineKind`
|
|
16
|
+
* (Story #3983).
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
|
-
import { componentMatches } from '../component-matcher.js';
|
|
13
19
|
import { canonicalise } from '../path-canon.js';
|
|
14
|
-
import {
|
|
20
|
+
import { makeBaselineKind } from './kind-factory.js';
|
|
15
21
|
|
|
16
22
|
export const name = 'coverage';
|
|
17
23
|
export const keyField = 'path';
|
|
18
|
-
const KERNEL_VERSION = '1.0.0';
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
return KERNEL_VERSION;
|
|
22
|
-
}
|
|
25
|
+
const COV_AXES = ['lines', 'branches', 'functions'];
|
|
23
26
|
|
|
24
27
|
export function projectRow(row) {
|
|
25
28
|
return {
|
|
@@ -35,160 +38,41 @@ function roundPct(v) {
|
|
|
35
38
|
return Number(v.toFixed(2));
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
function meanOf(rows, axis) {
|
|
42
|
+
let sum = 0;
|
|
43
|
+
for (const r of rows) sum += r[axis] ?? 0;
|
|
44
|
+
return Number((sum / rows.length).toFixed(2));
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
function aggregate(rows) {
|
|
43
48
|
if (!rows || rows.length === 0) {
|
|
44
49
|
return { lines: 0, branches: 0, functions: 0 };
|
|
45
50
|
}
|
|
46
|
-
let l = 0;
|
|
47
|
-
let b = 0;
|
|
48
|
-
let f = 0;
|
|
49
|
-
for (const row of rows) {
|
|
50
|
-
l += row.lines ?? 0;
|
|
51
|
-
b += row.branches ?? 0;
|
|
52
|
-
f += row.functions ?? 0;
|
|
53
|
-
}
|
|
54
51
|
return {
|
|
55
|
-
lines:
|
|
56
|
-
branches:
|
|
57
|
-
functions:
|
|
52
|
+
lines: meanOf(rows, 'lines'),
|
|
53
|
+
branches: meanOf(rows, 'branches'),
|
|
54
|
+
functions: meanOf(rows, 'functions'),
|
|
58
55
|
};
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
export function rollup(rows, components = []) {
|
|
62
|
-
const out = { '*': aggregate(rows) };
|
|
63
|
-
for (const c of components ?? []) {
|
|
64
|
-
const matched = (rows ?? []).filter((r) => componentMatches(c, r.path));
|
|
65
|
-
out[c.name] = aggregate(matched);
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Pure compare(head, base) for the coverage kind. Diffs rows by `path`.
|
|
72
|
-
* Higher percentages are better — a row regresses if any axis (lines,
|
|
73
|
-
* branches, functions) drops vs base; improves if any axis rises with no
|
|
74
|
-
* axis dropping; unchanged otherwise. New paths (head has a row that
|
|
75
|
-
* base lacks) land in the `additions` bucket; absolute-floor enforcement
|
|
76
|
-
* is the unified `check-baselines` gate's job and runs independently.
|
|
77
|
-
* Removed paths inherit a head of 100% so any lower base registers as
|
|
78
|
-
* an improvement.
|
|
79
|
-
*
|
|
80
|
-
* Story #2012 — sibling fix to maintainability.compare. The prior
|
|
81
|
-
* behaviour treated new paths as base=100% on every axis, so any partial
|
|
82
|
-
* coverage on a new file flipped to a regression.
|
|
83
|
-
*
|
|
84
|
-
* No I/O. No process exit. No friction emission.
|
|
85
|
-
*/
|
|
86
|
-
const COV_AXES = ['lines', 'branches', 'functions'];
|
|
87
|
-
|
|
88
|
-
export function compare(head, base) {
|
|
89
|
-
const headRows = Array.isArray(head?.rows) ? head.rows : [];
|
|
90
|
-
const baseRows = Array.isArray(base?.rows) ? base.rows : [];
|
|
91
|
-
const baseByKey = new Map();
|
|
92
|
-
for (const r of baseRows) baseByKey.set(r.path, r);
|
|
93
|
-
const seen = new Set();
|
|
94
|
-
const regressions = [];
|
|
95
|
-
const improvements = [];
|
|
96
|
-
const unchanged = [];
|
|
97
|
-
const additions = [];
|
|
98
|
-
for (const h of headRows) {
|
|
99
|
-
seen.add(h.path);
|
|
100
|
-
const b = baseByKey.get(h.path);
|
|
101
|
-
if (!b) {
|
|
102
|
-
additions.push({ key: h.path, head: h, base: null });
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
classifyCoverage(regressions, improvements, unchanged, h.path, h, b);
|
|
106
|
-
}
|
|
107
|
-
for (const b of baseRows) {
|
|
108
|
-
if (seen.has(b.path)) continue;
|
|
109
|
-
const h = perfectCoverageRow(b.path);
|
|
110
|
-
classifyCoverage(regressions, improvements, unchanged, b.path, h, b);
|
|
111
|
-
}
|
|
112
|
-
return { regressions, improvements, unchanged, additions };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
58
|
function perfectCoverageRow(path) {
|
|
116
59
|
return { path, lines: 100, branches: 100, functions: 100 };
|
|
117
60
|
}
|
|
118
61
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
else unchanged.push({ key, head, base });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Pure stabilizer for s-stability-epsilon (Story #1964). Folds sub-epsilon
|
|
141
|
-
* row deltas back to the prior bytes so env variance (e.g. ±0.05% jitter
|
|
142
|
-
* on a coverage axis) does not rewrite the on-disk baseline.
|
|
143
|
-
*
|
|
144
|
-
* For coverage, the comparison metric is the maximum |delta| across the
|
|
145
|
-
* three axes (lines, branches, functions). When the prior row exists and
|
|
146
|
-
* every axis delta is within `epsilon`, the prior row is returned
|
|
147
|
-
* verbatim; otherwise the regenerated row wins. Missing-prior rows always
|
|
148
|
-
* fall through to the regenerated row.
|
|
149
|
-
*
|
|
150
|
-
* No I/O. No mutation of inputs.
|
|
151
|
-
*
|
|
152
|
-
* @param {Array<{path: string, lines: number, branches: number, functions: number}>} prior
|
|
153
|
-
* @param {Array<{path: string, lines: number, branches: number, functions: number}>} regenerated
|
|
154
|
-
* @param {number} epsilon non-negative absolute tolerance (percentage points)
|
|
155
|
-
* @returns {Array<object>}
|
|
156
|
-
*/
|
|
157
|
-
export function applyEpsilon(prior, regenerated, epsilon) {
|
|
158
|
-
const priorRows = Array.isArray(prior) ? prior : [];
|
|
159
|
-
const regenRows = Array.isArray(regenerated) ? regenerated : [];
|
|
160
|
-
const eps = Number.isFinite(epsilon) && epsilon >= 0 ? epsilon : 0;
|
|
161
|
-
const priorByKey = new Map();
|
|
162
|
-
for (const r of priorRows) priorByKey.set(r.path, r);
|
|
163
|
-
return regenRows.map((row) => {
|
|
164
|
-
const p = priorByKey.get(row.path);
|
|
165
|
-
if (!p) return row;
|
|
166
|
-
let maxAxisDelta = 0;
|
|
167
|
-
for (const axis of COV_AXES) {
|
|
168
|
-
const d = Math.abs((row[axis] ?? 0) - (p[axis] ?? 0));
|
|
169
|
-
if (d > maxAxisDelta) maxAxisDelta = d;
|
|
170
|
-
}
|
|
171
|
-
return maxAxisDelta <= eps ? p : row;
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Pure scope-aware merge for s-diff-scoped-writes (Story #1974). Coverage
|
|
177
|
-
* rows match by `path`. In diff mode, rows whose `path` is OUTSIDE
|
|
178
|
-
* `scope.files` are preserved from `prior` verbatim; in-scope rows come
|
|
179
|
-
* from `regenerated`. In full mode (or no scope), regenerated wins
|
|
180
|
-
* everywhere. Pure; downstream `sortRows` re-sorts before write.
|
|
181
|
-
*
|
|
182
|
-
* @param {Array<{path: string, lines: number, branches: number, functions: number}>} prior
|
|
183
|
-
* @param {Array<{path: string, lines: number, branches: number, functions: number}>} regenerated
|
|
184
|
-
* @param {{mode: 'full'|'diff', files: Set<string>}|null|undefined} scope
|
|
185
|
-
* @returns {Array<object>}
|
|
186
|
-
*/
|
|
187
|
-
export function mergeRows(prior, regenerated, scope) {
|
|
188
|
-
return mergeRowsByScope({
|
|
189
|
-
prior,
|
|
190
|
-
regenerated,
|
|
191
|
-
scope,
|
|
192
|
-
scopeKey: (row) => row.path,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
62
|
+
export const {
|
|
63
|
+
kernelVersion,
|
|
64
|
+
sortRows,
|
|
65
|
+
rollup,
|
|
66
|
+
compare,
|
|
67
|
+
applyEpsilon,
|
|
68
|
+
mergeRows,
|
|
69
|
+
} = makeBaselineKind({
|
|
70
|
+
keyField,
|
|
71
|
+
kernelVersion: '1.0.0',
|
|
72
|
+
axes: COV_AXES,
|
|
73
|
+
betterWhen: 'higher',
|
|
74
|
+
aggregate,
|
|
75
|
+
missingBasePolicy: 'addition',
|
|
76
|
+
removedRowPolicy: { kind: 'perfect-head' },
|
|
77
|
+
perfectRow: perfectCoverageRow,
|
|
78
|
+
});
|
|
@@ -11,26 +11,25 @@
|
|
|
11
11
|
* per-file percentages (which would over-weight small files).
|
|
12
12
|
*
|
|
13
13
|
* Lower duplication is better, so the gate's floor direction is `lte`
|
|
14
|
-
* (see `check-baselines/phases/floors.js#axisDirection`).
|
|
14
|
+
* (see `check-baselines/phases/floors.js#axisDirection`). New paths land
|
|
15
|
+
* in the `additions` bucket (Story #2012); removed paths count as
|
|
16
|
+
* improvements when they carried any duplication — the file is gone, so
|
|
17
|
+
* its prior debt is gone too.
|
|
15
18
|
*
|
|
16
19
|
* `kernelVersion()` returns a static in-repo semver — the duplication
|
|
17
20
|
* scorer is the in-repo scan + rollup contract (the jscpd output is
|
|
18
21
|
* normalised before it reaches this module), so there is no upstream
|
|
19
22
|
* library version to track the way CRAP/MI track `typhonjs-escomplex`.
|
|
20
|
-
* Bump `
|
|
23
|
+
* Bump the version passed to `makeBaselineKind` whenever the row shape or
|
|
24
|
+
* rollup math changes. Scaffold is generated by `makeBaselineKind`
|
|
25
|
+
* (Story #3983).
|
|
21
26
|
*/
|
|
22
27
|
|
|
23
|
-
import { componentMatches } from '../component-matcher.js';
|
|
24
28
|
import { canonicalise } from '../path-canon.js';
|
|
25
|
-
import {
|
|
29
|
+
import { makeBaselineKind } from './kind-factory.js';
|
|
26
30
|
|
|
27
31
|
export const name = 'duplication';
|
|
28
32
|
export const keyField = 'path';
|
|
29
|
-
const KERNEL_VERSION = '1.0.0';
|
|
30
|
-
|
|
31
|
-
export function kernelVersion() {
|
|
32
|
-
return KERNEL_VERSION;
|
|
33
|
-
}
|
|
34
33
|
|
|
35
34
|
export function projectRow(row) {
|
|
36
35
|
const duplicatedLines = Number(row.duplicatedLines ?? 0);
|
|
@@ -47,10 +46,6 @@ export function projectRow(row) {
|
|
|
47
46
|
};
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
export function sortRows(rows) {
|
|
51
|
-
return [...rows].sort((a, b) => a.path.localeCompare(b.path));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
49
|
/**
|
|
55
50
|
* Exact aggregate duplication ratio: total duplicated lines / total lines
|
|
56
51
|
* across the row set, expressed as a percentage. Averaging per-file
|
|
@@ -92,106 +87,22 @@ function roundTo2(value) {
|
|
|
92
87
|
return Number(value.toFixed(2));
|
|
93
88
|
}
|
|
94
89
|
|
|
95
|
-
export
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* improvements when they carried any duplication; the file is gone, so its
|
|
115
|
-
* prior debt is gone too.
|
|
116
|
-
*
|
|
117
|
-
* No I/O. No process exit. No friction emission.
|
|
118
|
-
*/
|
|
119
|
-
export function compare(head, base) {
|
|
120
|
-
const headRows = Array.isArray(head?.rows) ? head.rows : [];
|
|
121
|
-
const baseRows = Array.isArray(base?.rows) ? base.rows : [];
|
|
122
|
-
const baseByKey = new Map();
|
|
123
|
-
for (const r of baseRows) baseByKey.set(r.path, r);
|
|
124
|
-
const seen = new Set();
|
|
125
|
-
const regressions = [];
|
|
126
|
-
const improvements = [];
|
|
127
|
-
const unchanged = [];
|
|
128
|
-
const additions = [];
|
|
129
|
-
for (const h of headRows) {
|
|
130
|
-
seen.add(h.path);
|
|
131
|
-
const b = baseByKey.get(h.path);
|
|
132
|
-
if (!b) {
|
|
133
|
-
additions.push({ key: h.path, head: h, base: null });
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
const delta = (h.percentage ?? 0) - (b.percentage ?? 0);
|
|
137
|
-
if (delta > 0) regressions.push({ key: h.path, head: h, base: b });
|
|
138
|
-
else if (delta < 0) improvements.push({ key: h.path, head: h, base: b });
|
|
139
|
-
else unchanged.push({ key: h.path, head: h, base: b });
|
|
140
|
-
}
|
|
141
|
-
for (const b of baseRows) {
|
|
142
|
-
if (seen.has(b.path)) continue;
|
|
143
|
-
if ((b.percentage ?? 0) > 0) {
|
|
144
|
-
improvements.push({ key: b.path, head: null, base: b });
|
|
145
|
-
} else {
|
|
146
|
-
unchanged.push({ key: b.path, head: null, base: b });
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return { regressions, improvements, unchanged, additions };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Pure stabilizer for s-stability-epsilon (Story #1964). Duplication rows
|
|
154
|
-
* match by `path`. The metric is the absolute `percentage` delta. Sub-
|
|
155
|
-
* epsilon deltas resolve to the prior bytes so env variance never rewrites
|
|
156
|
-
* the on-disk baseline; missing-prior rows fall through.
|
|
157
|
-
*
|
|
158
|
-
* @param {Array<{path: string, percentage: number}>} prior
|
|
159
|
-
* @param {Array<{path: string, percentage: number}>} regenerated
|
|
160
|
-
* @param {number} epsilon non-negative absolute tolerance on percentage
|
|
161
|
-
* @returns {Array<object>}
|
|
162
|
-
*/
|
|
163
|
-
export function applyEpsilon(prior, regenerated, epsilon) {
|
|
164
|
-
const priorRows = Array.isArray(prior) ? prior : [];
|
|
165
|
-
const regenRows = Array.isArray(regenerated) ? regenerated : [];
|
|
166
|
-
const eps = Number.isFinite(epsilon) && epsilon >= 0 ? epsilon : 0;
|
|
167
|
-
const priorByKey = new Map();
|
|
168
|
-
for (const r of priorRows) priorByKey.set(r.path, r);
|
|
169
|
-
return regenRows.map((row) => {
|
|
170
|
-
const p = priorByKey.get(row.path);
|
|
171
|
-
if (!p) return row;
|
|
172
|
-
return Math.abs((row.percentage ?? 0) - (p.percentage ?? 0)) <= eps
|
|
173
|
-
? p
|
|
174
|
-
: row;
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Pure scope-aware merge for s-diff-scoped-writes (Story #1974).
|
|
180
|
-
* Duplication rows match by `path`. In diff mode, rows whose `path` is
|
|
181
|
-
* OUTSIDE `scope.files` are preserved from `prior` verbatim; in-scope rows
|
|
182
|
-
* come from `regenerated`. In full mode (or no scope), regenerated wins
|
|
183
|
-
* everywhere.
|
|
184
|
-
*
|
|
185
|
-
* @param {Array<{path: string, percentage: number}>} prior
|
|
186
|
-
* @param {Array<{path: string, percentage: number}>} regenerated
|
|
187
|
-
* @param {{mode: 'full'|'diff', files: Set<string>}|null|undefined} scope
|
|
188
|
-
* @returns {Array<object>}
|
|
189
|
-
*/
|
|
190
|
-
export function mergeRows(prior, regenerated, scope) {
|
|
191
|
-
return mergeRowsByScope({
|
|
192
|
-
prior,
|
|
193
|
-
regenerated,
|
|
194
|
-
scope,
|
|
195
|
-
scopeKey: (row) => row.path,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
90
|
+
export const {
|
|
91
|
+
kernelVersion,
|
|
92
|
+
sortRows,
|
|
93
|
+
rollup,
|
|
94
|
+
compare,
|
|
95
|
+
applyEpsilon,
|
|
96
|
+
mergeRows,
|
|
97
|
+
} = makeBaselineKind({
|
|
98
|
+
keyField,
|
|
99
|
+
kernelVersion: '1.0.0',
|
|
100
|
+
axes: ['percentage'],
|
|
101
|
+
betterWhen: 'lower',
|
|
102
|
+
aggregate,
|
|
103
|
+
missingBasePolicy: 'addition',
|
|
104
|
+
removedRowPolicy: {
|
|
105
|
+
kind: 'improvement-when',
|
|
106
|
+
when: (b) => (b.percentage ?? 0) > 0,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kinds/kind-factory.js — shared scaffold factory for per-kind baseline
|
|
3
|
+
* modules (Story #3983).
|
|
4
|
+
*
|
|
5
|
+
* The five row-metric kinds (coverage, mutation, maintainability,
|
|
6
|
+
* lighthouse, duplication) used to hand-roll the same scaffold each:
|
|
7
|
+
* `kernelVersion`, `sortRows`, a component-aware `rollup`, a
|
|
8
|
+
* `compare(head, base)` that diffs rows by key into
|
|
9
|
+
* `{regressions, improvements, unchanged, additions}`, an
|
|
10
|
+
* `applyEpsilon` stabilizer (Story #1964), and a scope-aware
|
|
11
|
+
* `mergeRows` (Story #1974). Only the axis list, direction-of-better,
|
|
12
|
+
* aggregate math, and the missing/removed-row policies differ per kind.
|
|
13
|
+
*
|
|
14
|
+
* `makeBaselineKind` generates the scaffold once; each kind module stays
|
|
15
|
+
* a thin parameterization with a byte-identical exported surface. The
|
|
16
|
+
* Story #2012 class of fix ("new paths must land in `additions`, never
|
|
17
|
+
* the regression arm") lives here exactly once.
|
|
18
|
+
*
|
|
19
|
+
* All generated functions are pure: no I/O, no process exit, no
|
|
20
|
+
* friction emission.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { componentMatches } from '../component-matcher.js';
|
|
24
|
+
import { mergeRowsByScope } from '../scope.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the shared scaffold for a per-kind baseline module.
|
|
28
|
+
*
|
|
29
|
+
* @param {{
|
|
30
|
+
* keyField: string,
|
|
31
|
+
* kernelVersion: string | (() => string),
|
|
32
|
+
* axes: string[],
|
|
33
|
+
* betterWhen: 'higher' | 'lower',
|
|
34
|
+
* aggregate: (rows: object[]) => Record<string, number>,
|
|
35
|
+
* missingBasePolicy?: 'addition' | 'perfect',
|
|
36
|
+
* removedRowPolicy?:
|
|
37
|
+
* | { kind: 'perfect-head' }
|
|
38
|
+
* | { kind: 'improvement-when', when: (row: object) => boolean },
|
|
39
|
+
* perfectRow?: (key: string) => object,
|
|
40
|
+
* }} opts
|
|
41
|
+
* - `keyField` — row identity property (`'path'` or `'route'`)
|
|
42
|
+
* - `kernelVersion` — static semver, or a thunk for kinds that pin
|
|
43
|
+
* to another kind's kernel (MI → CRAP)
|
|
44
|
+
* - `axes` — metric property names compared per row
|
|
45
|
+
* - `betterWhen` — `'higher'` (coverage, MI, …) or `'lower'`
|
|
46
|
+
* (duplication): decides which delta sign is a
|
|
47
|
+
* regression
|
|
48
|
+
* - `aggregate` — per-kind rollup math over a row set
|
|
49
|
+
* - `missingBasePolicy` — head row with no base row: `'addition'`
|
|
50
|
+
* (default; Story #2012 bucket) or `'perfect'`
|
|
51
|
+
* (classify against a perfect base — lighthouse,
|
|
52
|
+
* where a new route must meet the bar)
|
|
53
|
+
* - `removedRowPolicy` — base row with no head row: `'perfect-head'`
|
|
54
|
+
* classifies against a perfect head row;
|
|
55
|
+
* `'improvement-when'` pushes an improvement
|
|
56
|
+
* when `when(baseRow)` holds, else unchanged
|
|
57
|
+
* - `perfectRow` — builds the perfect row for the policies above
|
|
58
|
+
* @returns {{
|
|
59
|
+
* kernelVersion: () => string,
|
|
60
|
+
* sortRows: (rows: object[]) => object[],
|
|
61
|
+
* rollup: (rows: object[], components?: object[]) => Record<string, object>,
|
|
62
|
+
* compare: (head: object, base: object) => object,
|
|
63
|
+
* applyEpsilon: (prior: object[], regenerated: object[], epsilon: number) => object[],
|
|
64
|
+
* mergeRows: (prior: object[], regenerated: object[], scope: object) => object[],
|
|
65
|
+
* }}
|
|
66
|
+
*/
|
|
67
|
+
export function makeBaselineKind({
|
|
68
|
+
keyField,
|
|
69
|
+
kernelVersion,
|
|
70
|
+
axes,
|
|
71
|
+
betterWhen,
|
|
72
|
+
aggregate,
|
|
73
|
+
missingBasePolicy = 'addition',
|
|
74
|
+
removedRowPolicy = { kind: 'improvement-when', when: () => false },
|
|
75
|
+
perfectRow = null,
|
|
76
|
+
}) {
|
|
77
|
+
const keyOf = (row) => row[keyField];
|
|
78
|
+
const kernelVersionFn =
|
|
79
|
+
typeof kernelVersion === 'function' ? kernelVersion : () => kernelVersion;
|
|
80
|
+
|
|
81
|
+
function sortRows(rows) {
|
|
82
|
+
return [...rows].sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rollup(rows, components = []) {
|
|
86
|
+
const out = { '*': aggregate(rows) };
|
|
87
|
+
for (const c of components ?? []) {
|
|
88
|
+
const matched = (rows ?? []).filter((r) => componentMatches(c, keyOf(r)));
|
|
89
|
+
out[c.name] = aggregate(matched);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function classify(regressions, improvements, unchanged, key, head, base) {
|
|
95
|
+
let down = false;
|
|
96
|
+
let up = false;
|
|
97
|
+
for (const axis of axes) {
|
|
98
|
+
const delta = (head[axis] ?? 0) - (base[axis] ?? 0);
|
|
99
|
+
const worse = betterWhen === 'higher' ? delta < 0 : delta > 0;
|
|
100
|
+
const better = betterWhen === 'higher' ? delta > 0 : delta < 0;
|
|
101
|
+
if (worse) down = true;
|
|
102
|
+
else if (better) up = true;
|
|
103
|
+
}
|
|
104
|
+
if (down) regressions.push({ key, head, base });
|
|
105
|
+
else if (up) improvements.push({ key, head, base });
|
|
106
|
+
else unchanged.push({ key, head, base });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function compare(head, base) {
|
|
110
|
+
const headRows = Array.isArray(head?.rows) ? head.rows : [];
|
|
111
|
+
const baseRows = Array.isArray(base?.rows) ? base.rows : [];
|
|
112
|
+
const baseByKey = new Map();
|
|
113
|
+
for (const r of baseRows) baseByKey.set(keyOf(r), r);
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
const regressions = [];
|
|
116
|
+
const improvements = [];
|
|
117
|
+
const unchanged = [];
|
|
118
|
+
const additions = [];
|
|
119
|
+
for (const h of headRows) {
|
|
120
|
+
const key = keyOf(h);
|
|
121
|
+
seen.add(key);
|
|
122
|
+
const b = baseByKey.get(key);
|
|
123
|
+
if (!b) {
|
|
124
|
+
if (missingBasePolicy === 'addition') {
|
|
125
|
+
additions.push({ key, head: h, base: null });
|
|
126
|
+
} else {
|
|
127
|
+
classify(
|
|
128
|
+
regressions,
|
|
129
|
+
improvements,
|
|
130
|
+
unchanged,
|
|
131
|
+
key,
|
|
132
|
+
h,
|
|
133
|
+
perfectRow(key),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
classify(regressions, improvements, unchanged, key, h, b);
|
|
139
|
+
}
|
|
140
|
+
for (const b of baseRows) {
|
|
141
|
+
const key = keyOf(b);
|
|
142
|
+
if (seen.has(key)) continue;
|
|
143
|
+
if (removedRowPolicy.kind === 'perfect-head') {
|
|
144
|
+
classify(regressions, improvements, unchanged, key, perfectRow(key), b);
|
|
145
|
+
} else if (removedRowPolicy.when(b)) {
|
|
146
|
+
improvements.push({ key, head: null, base: b });
|
|
147
|
+
} else {
|
|
148
|
+
unchanged.push({ key, head: null, base: b });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (missingBasePolicy === 'addition') {
|
|
152
|
+
return { regressions, improvements, unchanged, additions };
|
|
153
|
+
}
|
|
154
|
+
return { regressions, improvements, unchanged };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function applyEpsilon(prior, regenerated, epsilon) {
|
|
158
|
+
const priorRows = Array.isArray(prior) ? prior : [];
|
|
159
|
+
const regenRows = Array.isArray(regenerated) ? regenerated : [];
|
|
160
|
+
const eps = Number.isFinite(epsilon) && epsilon >= 0 ? epsilon : 0;
|
|
161
|
+
const priorByKey = new Map();
|
|
162
|
+
for (const r of priorRows) priorByKey.set(keyOf(r), r);
|
|
163
|
+
return regenRows.map((row) => {
|
|
164
|
+
const p = priorByKey.get(keyOf(row));
|
|
165
|
+
if (!p) return row;
|
|
166
|
+
let maxAxisDelta = 0;
|
|
167
|
+
for (const axis of axes) {
|
|
168
|
+
const d = Math.abs((row[axis] ?? 0) - (p[axis] ?? 0));
|
|
169
|
+
if (d > maxAxisDelta) maxAxisDelta = d;
|
|
170
|
+
}
|
|
171
|
+
return maxAxisDelta <= eps ? p : row;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function mergeRows(prior, regenerated, scope) {
|
|
176
|
+
return mergeRowsByScope({
|
|
177
|
+
prior,
|
|
178
|
+
regenerated,
|
|
179
|
+
scope,
|
|
180
|
+
scopeKey: keyOf,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
kernelVersion: kernelVersionFn,
|
|
186
|
+
sortRows,
|
|
187
|
+
rollup,
|
|
188
|
+
compare,
|
|
189
|
+
applyEpsilon,
|
|
190
|
+
mergeRows,
|
|
191
|
+
};
|
|
192
|
+
}
|