session-orchestrator 4.2.0 → 5.0.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/skills/session-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +94 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- package/commands/ux-grill.md +51 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/codex-setup.md +8 -0
- package/docs/components.md +7 -7
- package/docs/events-schema.md +9 -5
- package/docs/github-mirror-protection.md +61 -20
- package/docs/migration-v5.md +62 -0
- package/docs/scope-collision-guard.md +14 -0
- package/hooks/_lib/hook-import-set.json +44 -2
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +190 -3
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +51 -4
- package/package.json +2 -1
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- package/scripts/lib/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- package/scripts/lib/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +114 -13
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget.mjs +336 -6
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +11 -1
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +14 -65
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/scope-echo.mjs +346 -0
- package/scripts/lib/session-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-start-probes.mjs +419 -53
- package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +48 -20
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- package/scripts/mcp-server.sh +16 -1
- package/scripts/release.mjs +7 -2
- package/skills/bootstrap/SKILL.md +12 -209
- package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +7 -3
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- package/skills/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +10 -0
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +4 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-5-issue-cleanup.md +6 -1
- package/skills/session-end/session-metrics-write.md +2 -0
- package/skills/session-plan/SKILL.md +2 -144
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-start/SKILL.md +24 -6
- package/skills/session-start/references/operations-contract.md +114 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +22 -20
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +3 -128
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
- package/skills/wave-executor/references/wave-loop-review.md +18 -5
- package/templates/_shared/ux-manifest.template.md +149 -0
- package/templates/_shared/journey-manifest.md +0 -114
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ux-grill/compare.mjs — cross-run fingerprint classification (PRD § 2 S5,
|
|
3
|
+
* § 3 "Reconcile & Regression").
|
|
4
|
+
*
|
|
5
|
+
* Two layers, deliberately split:
|
|
6
|
+
* - {@link compareFingerprints} is PURE — two arrays in, three arrays out. It
|
|
7
|
+
* is the whole classification rule and needs no filesystem to be tested.
|
|
8
|
+
* - {@link compareRuns} is the thin I/O wrapper: it loads the current run's
|
|
9
|
+
* findings, locates the baseline run via `run-record.mjs`, applies the pure
|
|
10
|
+
* function, subtracts the UNMEASURED set, and patches the run-record's
|
|
11
|
+
* `compare` counts.
|
|
12
|
+
*
|
|
13
|
+
* Imports `./run-record.mjs` only (which is itself fs + `./paths.mjs` +
|
|
14
|
+
* `./schema.mjs`). No shell-out, no network.
|
|
15
|
+
*
|
|
16
|
+
* THE TWO WAYS A `fixed` CLASSIFICATION LIES, both surfaced rather than hidden:
|
|
17
|
+
*
|
|
18
|
+
* 1. A rule left the RUBRIC. If the baseline ran under a different
|
|
19
|
+
* `rubric_hash`, a fingerprint that disappeared may no longer be
|
|
20
|
+
* checked for. `compareRuns` still compares (refusing to compare would
|
|
21
|
+
* lose the persisting set too) but returns `rubricChanged: true`, and the
|
|
22
|
+
* `fixed` array then carries FIXED-OR-RUBRIC-CHANGED semantics — a fact
|
|
23
|
+
* for the caller to render, never a filter applied here.
|
|
24
|
+
*
|
|
25
|
+
* 2. A measurement never RAN. A route whose `open` failed, a viewport whose
|
|
26
|
+
* `set device` did not take, one `eval` that threw — every such step is a
|
|
27
|
+
* `skipped[]` entry in the run record precisely because zero findings for
|
|
28
|
+
* a check that never ran is indistinguishable from a clean check
|
|
29
|
+
* (`schema.mjs` § SKIP_REASONS). Previous findings under a skipped scope
|
|
30
|
+
* are therefore removed from `fixed` and returned as `unmeasured`.
|
|
31
|
+
*
|
|
32
|
+
* Exports:
|
|
33
|
+
* compareFingerprints(previousFindings, currentFindings)
|
|
34
|
+
* compareRuns({repoRoot, runId, manifestHash, build, rubricHash, skipped})
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
findPreviousRun,
|
|
39
|
+
readFindings,
|
|
40
|
+
readRunRecords,
|
|
41
|
+
updateRunRecordCompare,
|
|
42
|
+
} from './run-record.mjs';
|
|
43
|
+
|
|
44
|
+
/** Sort findings by fingerprint so every consumer sees one deterministic order. */
|
|
45
|
+
function byFingerprint(findings) {
|
|
46
|
+
return [...findings].sort((a, b) =>
|
|
47
|
+
a.fingerprint < b.fingerprint ? -1 : a.fingerprint > b.fingerprint ? 1 : 0,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Index findings by fingerprint. A duplicate fingerprint keeps the FIRST entry:
|
|
53
|
+
* `collect()` writes at most one finding per (checkId, locator) pair, so a
|
|
54
|
+
* duplicate is a malformed input, and "first wins" makes the result independent
|
|
55
|
+
* of file order.
|
|
56
|
+
*
|
|
57
|
+
* @param {Array<{fingerprint: string}>} findings
|
|
58
|
+
* @returns {Map<string, object>}
|
|
59
|
+
*/
|
|
60
|
+
function indexByFingerprint(findings) {
|
|
61
|
+
const map = new Map();
|
|
62
|
+
for (const finding of findings) {
|
|
63
|
+
const fp = finding?.fingerprint;
|
|
64
|
+
if (typeof fp !== 'string' || fp.length === 0) continue;
|
|
65
|
+
if (!map.has(fp)) map.set(fp, finding);
|
|
66
|
+
}
|
|
67
|
+
return map;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Classify every fingerprint of two runs as `new`, `persisting` or `fixed`.
|
|
72
|
+
*
|
|
73
|
+
* Pure. Knows nothing about skips or rubric hashes — the caller subtracts those
|
|
74
|
+
* (see {@link compareRuns}), because they are facts about the RUN, not about the
|
|
75
|
+
* fingerprint sets.
|
|
76
|
+
*
|
|
77
|
+
* @param {Array<{fingerprint: string}>} previousFindings - the baseline run's findings
|
|
78
|
+
* @param {Array<{fingerprint: string}>} currentFindings - this run's findings
|
|
79
|
+
* @returns {{new: object[], persisting: object[], fixed: object[]}} FINDING
|
|
80
|
+
* objects, each array sorted by fingerprint. `new` and `persisting` carry the
|
|
81
|
+
* CURRENT finding (its severity/evidence are the ones that matter now);
|
|
82
|
+
* `fixed` carries the PREVIOUS finding, which is the only record of it left.
|
|
83
|
+
* @throws {TypeError} if either argument is not an array
|
|
84
|
+
*/
|
|
85
|
+
export function compareFingerprints(previousFindings, currentFindings) {
|
|
86
|
+
if (!Array.isArray(previousFindings)) {
|
|
87
|
+
throw new TypeError('compareFingerprints: previousFindings must be an array');
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(currentFindings)) {
|
|
90
|
+
throw new TypeError('compareFingerprints: currentFindings must be an array');
|
|
91
|
+
}
|
|
92
|
+
const previous = indexByFingerprint(previousFindings);
|
|
93
|
+
const current = indexByFingerprint(currentFindings);
|
|
94
|
+
|
|
95
|
+
const fresh = [];
|
|
96
|
+
const persisting = [];
|
|
97
|
+
for (const [fp, finding] of current) {
|
|
98
|
+
(previous.has(fp) ? persisting : fresh).push(finding);
|
|
99
|
+
}
|
|
100
|
+
const fixed = [];
|
|
101
|
+
for (const [fp, finding] of previous) {
|
|
102
|
+
if (!current.has(fp)) fixed.push(finding);
|
|
103
|
+
}
|
|
104
|
+
return { new: byFingerprint(fresh), persisting: byFingerprint(persisting), fixed: byFingerprint(fixed) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse ONE `skipped[].what` into the locator scope it invalidates.
|
|
109
|
+
*
|
|
110
|
+
* `collect.mjs` emits exactly five `what` shapes (measured 2026-09-12 against
|
|
111
|
+
* `scripts/lib/ux-grill/collect.mjs` — `skipEntry(` call sites at :745, :748,
|
|
112
|
+
* :765, :774, :991):
|
|
113
|
+
*
|
|
114
|
+
* `viewport:<vp>` device-mismatch → the whole viewport
|
|
115
|
+
* `viewport:<vp>|eval:viewport-width` measure-failed → the whole viewport
|
|
116
|
+
* `route:<path>|<vp>` route-unreachable→ that route × viewport
|
|
117
|
+
* `<path>|<vp>|<call>` measure-failed → that route × viewport
|
|
118
|
+
* `journey:<name>` route-unreachable→ that journey, all viewports
|
|
119
|
+
*
|
|
120
|
+
* The last shape carries no viewport, so it invalidates the journey on EVERY
|
|
121
|
+
* viewport — widening rather than guessing, because the cost of a
|
|
122
|
+
* false `unmeasured` is a finding reported as still-open, while the cost of a
|
|
123
|
+
* missed one is an outage reported as `fixed`.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} what
|
|
126
|
+
* @returns {{kind: 'viewport', viewport: string}
|
|
127
|
+
* | {kind: 'route', route: string, viewport: string|null}
|
|
128
|
+
* | {kind: 'journey', name: string}
|
|
129
|
+
* | null} `null` for an unrecognised shape (it invalidates nothing)
|
|
130
|
+
*/
|
|
131
|
+
function skipScope(what) {
|
|
132
|
+
if (typeof what !== 'string' || what.length === 0) return null;
|
|
133
|
+
const segments = what.split('|');
|
|
134
|
+
const head = segments[0];
|
|
135
|
+
if (head.startsWith('viewport:')) {
|
|
136
|
+
const viewport = head.slice('viewport:'.length);
|
|
137
|
+
return viewport.length > 0 ? { kind: 'viewport', viewport } : null;
|
|
138
|
+
}
|
|
139
|
+
if (head.startsWith('route:')) {
|
|
140
|
+
const route = head.slice('route:'.length);
|
|
141
|
+
return route.length > 0 ? { kind: 'route', route, viewport: segments[1] ?? null } : null;
|
|
142
|
+
}
|
|
143
|
+
if (head.startsWith('journey:')) {
|
|
144
|
+
const name = head.slice('journey:'.length);
|
|
145
|
+
return name.length > 0 ? { kind: 'journey', name } : null;
|
|
146
|
+
}
|
|
147
|
+
// Bare `<route>|<viewport>|<call>` — the measure-failed shape.
|
|
148
|
+
if (segments.length >= 2 && head.length > 0) {
|
|
149
|
+
return { kind: 'route', route: head, viewport: segments[1] };
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build a predicate over finding LOCATORS from a run's `skipped[]`.
|
|
156
|
+
*
|
|
157
|
+
* Finding locators are `route|viewport|selector` or `journey|viewport|<name>`
|
|
158
|
+
* (`schema.mjs` § makeFinding).
|
|
159
|
+
*
|
|
160
|
+
* @param {Array<{what: string, reason: string}>} skipped
|
|
161
|
+
* @returns {(locator: string) => boolean} true when the locator sits under a
|
|
162
|
+
* scope this run did not measure
|
|
163
|
+
*/
|
|
164
|
+
function unmeasuredMatcher(skipped) {
|
|
165
|
+
const scopes = [];
|
|
166
|
+
for (const entry of Array.isArray(skipped) ? skipped : []) {
|
|
167
|
+
const scope = skipScope(entry?.what);
|
|
168
|
+
if (scope) scopes.push(scope);
|
|
169
|
+
}
|
|
170
|
+
if (scopes.length === 0) return () => false;
|
|
171
|
+
return (locator) => {
|
|
172
|
+
if (typeof locator !== 'string') return false;
|
|
173
|
+
const segments = locator.split('|');
|
|
174
|
+
for (const scope of scopes) {
|
|
175
|
+
if (scope.kind === 'viewport' && segments[1] === scope.viewport) return true;
|
|
176
|
+
if (
|
|
177
|
+
scope.kind === 'route' &&
|
|
178
|
+
segments[0] === scope.route &&
|
|
179
|
+
(scope.viewport === null || segments[1] === scope.viewport)
|
|
180
|
+
) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
if (scope.kind === 'journey' && segments[0] === 'journey' && segments[2] === scope.name) return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Compare one run against its baseline and patch the run-record's counts.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} opts
|
|
193
|
+
* @param {string} opts.repoRoot - absolute repo root of the TARGET repo
|
|
194
|
+
* @param {string} opts.runId - the CURRENT run
|
|
195
|
+
* @param {string} [opts.manifestHash] - compare key; read from the current run's
|
|
196
|
+
* ledger record when omitted
|
|
197
|
+
* @param {string} [opts.build] - `'dev'`/`'prod'`; read from the record when omitted
|
|
198
|
+
* @param {string} [opts.rubricHash] - this run's rubric hash; read from the record
|
|
199
|
+
* when omitted. Drives `rubricChanged` only — never the baseline selection.
|
|
200
|
+
* @param {Array<{what: string, reason: string}>} [opts.skipped] - this run's
|
|
201
|
+
* skips; read from the record when omitted
|
|
202
|
+
* @returns {{
|
|
203
|
+
* runId: string, previousRunId: string|null, baseline: boolean, rubricChanged: boolean,
|
|
204
|
+
* new: object[], persisting: object[], fixed: object[], unmeasured: object[],
|
|
205
|
+
* counts: {new: number, persisting: number, fixed: number},
|
|
206
|
+
* recordUpdated: number, skippedLines: {current: number, previous: number, ledger: number}
|
|
207
|
+
* }} `fixed` carries FIXED-OR-RUBRIC-CHANGED semantics whenever
|
|
208
|
+
* `rubricChanged` is true (see module header). `unmeasured` carries previous
|
|
209
|
+
* findings whose route/viewport/journey this run did not measure — they are
|
|
210
|
+
* NOT in `fixed` and NOT in `persisting`.
|
|
211
|
+
* @throws {TypeError} when `manifestHash`/`build` can be resolved from neither
|
|
212
|
+
* the arguments nor the ledger — comparing without a compare key would silently
|
|
213
|
+
* pick an unrelated run as the baseline.
|
|
214
|
+
*/
|
|
215
|
+
export function compareRuns({ repoRoot, runId, manifestHash, build, rubricHash, skipped } = {}) {
|
|
216
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
|
|
217
|
+
throw new TypeError('compareRuns: repoRoot must be a non-empty string');
|
|
218
|
+
}
|
|
219
|
+
if (typeof runId !== 'string' || runId.length === 0) {
|
|
220
|
+
throw new TypeError('compareRuns: runId must be a non-empty string');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const { records, skippedLines: ledgerSkippedLines } = readRunRecords(repoRoot, { limit: Infinity });
|
|
224
|
+
const currentRecord = records.find((record) => record.run_id === runId) ?? null;
|
|
225
|
+
|
|
226
|
+
const effectiveManifestHash = manifestHash ?? currentRecord?.manifest_hash;
|
|
227
|
+
const effectiveBuild = build ?? currentRecord?.build;
|
|
228
|
+
const effectiveRubricHash = rubricHash ?? currentRecord?.rubric_hash ?? null;
|
|
229
|
+
const effectiveSkipped = skipped ?? currentRecord?.skipped ?? [];
|
|
230
|
+
if (typeof effectiveManifestHash !== 'string' || effectiveManifestHash.length === 0) {
|
|
231
|
+
throw new TypeError(`compareRuns: no manifestHash for run ${runId} — pass one or append its run-record first`);
|
|
232
|
+
}
|
|
233
|
+
if (typeof effectiveBuild !== 'string' || effectiveBuild.length === 0) {
|
|
234
|
+
throw new TypeError(`compareRuns: no build for run ${runId} — pass one or append its run-record first`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const currentRead = readFindings(repoRoot, runId);
|
|
238
|
+
const previousRecord = findPreviousRun(repoRoot, {
|
|
239
|
+
manifestHash: effectiveManifestHash,
|
|
240
|
+
build: effectiveBuild,
|
|
241
|
+
beforeRunId: runId,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
let result;
|
|
245
|
+
let previousRead = { findings: [], skippedLines: 0 };
|
|
246
|
+
if (previousRecord === null) {
|
|
247
|
+
// Baseline run: everything measured is new by definition. Not "clean" —
|
|
248
|
+
// a first run with 40 findings has 40 new ones.
|
|
249
|
+
result = { new: byFingerprint(currentRead.findings), persisting: [], fixed: [] };
|
|
250
|
+
} else {
|
|
251
|
+
previousRead = readFindings(repoRoot, previousRecord.run_id);
|
|
252
|
+
result = compareFingerprints(previousRead.findings, currentRead.findings);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const isUnmeasured = unmeasuredMatcher(effectiveSkipped);
|
|
256
|
+
const unmeasured = [];
|
|
257
|
+
const fixed = [];
|
|
258
|
+
for (const finding of result.fixed) {
|
|
259
|
+
(isUnmeasured(finding?.locator) ? unmeasured : fixed).push(finding);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const counts = { new: result.new.length, persisting: result.persisting.length, fixed: fixed.length };
|
|
263
|
+
const { updated } = updateRunRecordCompare(repoRoot, runId, counts);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
runId,
|
|
267
|
+
previousRunId: previousRecord?.run_id ?? null,
|
|
268
|
+
baseline: previousRecord === null,
|
|
269
|
+
rubricChanged:
|
|
270
|
+
previousRecord !== null &&
|
|
271
|
+
effectiveRubricHash !== null &&
|
|
272
|
+
previousRecord.rubric_hash !== effectiveRubricHash,
|
|
273
|
+
new: result.new,
|
|
274
|
+
persisting: result.persisting,
|
|
275
|
+
fixed,
|
|
276
|
+
unmeasured,
|
|
277
|
+
counts,
|
|
278
|
+
recordUpdated: updated,
|
|
279
|
+
skippedLines: {
|
|
280
|
+
current: currentRead.skippedLines,
|
|
281
|
+
previous: previousRead.skippedLines,
|
|
282
|
+
ledger: ledgerSkippedLines,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|