@spexcode/spec-eval 0.6.5
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/dist/cache.d.ts +10 -0
- package/dist/cache.js +50 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +981 -0
- package/dist/evaltab.d.ts +98 -0
- package/dist/evaltab.js +176 -0
- package/dist/filing.d.ts +16 -0
- package/dist/filing.js +36 -0
- package/dist/freshness.d.ts +48 -0
- package/dist/freshness.js +799 -0
- package/dist/host.d.ts +46 -0
- package/dist/host.js +53 -0
- package/dist/humanok.d.ts +11 -0
- package/dist/humanok.js +27 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/remarks.d.ts +31 -0
- package/dist/remarks.js +1 -0
- package/dist/scenariofresh.d.ts +13 -0
- package/dist/scenariofresh.js +303 -0
- package/dist/scenarios.d.ts +99 -0
- package/dist/scenarios.js +650 -0
- package/dist/sessioneval.d.ts +307 -0
- package/dist/sessioneval.js +1883 -0
- package/dist/sidecar.d.ts +55 -0
- package/dist/sidecar.js +82 -0
- package/dist/timeline.d.ts +25 -0
- package/dist/timeline.js +65 -0
- package/dist/ui-path.d.ts +1 -0
- package/dist/ui-path.js +2 -0
- package/package.json +36 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type DriftIndex, type HistoryIndex } from '@spexcode/spec-core';
|
|
2
|
+
import { loadSpecs } from '@spexcode/spec-core';
|
|
3
|
+
import type { RemarkTrack, Issue } from './remarks.js';
|
|
4
|
+
export type { RemarkTrack, Issue, Reply } from './remarks.js';
|
|
5
|
+
import { type EvalNode, type ScenarioTestReference } from './scenarios.js';
|
|
6
|
+
import { type Verdict, type EvidenceKind, type Retraction } from './sidecar.js';
|
|
7
|
+
import { type StaleAxis } from './freshness.js';
|
|
8
|
+
import { type ScenarioIndex } from './scenariofresh.js';
|
|
9
|
+
export type EvidenceView = {
|
|
10
|
+
hash: string;
|
|
11
|
+
kind: EvidenceKind;
|
|
12
|
+
state: 'present' | 'miss';
|
|
13
|
+
};
|
|
14
|
+
export type RemarkView = {
|
|
15
|
+
rid: string;
|
|
16
|
+
ref: string;
|
|
17
|
+
by: string;
|
|
18
|
+
at: string;
|
|
19
|
+
body: string;
|
|
20
|
+
targetCodeSha: string;
|
|
21
|
+
resolved: boolean;
|
|
22
|
+
resolvedAt?: string;
|
|
23
|
+
resolvedBy?: string;
|
|
24
|
+
dangling: boolean;
|
|
25
|
+
};
|
|
26
|
+
export type EvalEntry = {
|
|
27
|
+
scenario: string;
|
|
28
|
+
expected: string;
|
|
29
|
+
codeSha: string;
|
|
30
|
+
evidence?: EvidenceView[];
|
|
31
|
+
blob: string | null;
|
|
32
|
+
blobKind?: EvidenceKind;
|
|
33
|
+
timelineBlob?: string;
|
|
34
|
+
evaluator?: string;
|
|
35
|
+
by?: string;
|
|
36
|
+
verdict?: Verdict;
|
|
37
|
+
ts: string;
|
|
38
|
+
fresh: boolean;
|
|
39
|
+
staleAxes: StaleAxis[];
|
|
40
|
+
freshnessDeferred?: true;
|
|
41
|
+
blobState: 'present' | 'miss' | 'none';
|
|
42
|
+
codeDrift?: {
|
|
43
|
+
file: string;
|
|
44
|
+
behind: number;
|
|
45
|
+
}[];
|
|
46
|
+
remarks?: RemarkView[];
|
|
47
|
+
thread?: Issue;
|
|
48
|
+
humanOk?: {
|
|
49
|
+
by: string;
|
|
50
|
+
ts: string;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export type DanglingTrack = {
|
|
54
|
+
scenario: string;
|
|
55
|
+
threadId: string;
|
|
56
|
+
thread: Issue;
|
|
57
|
+
remarks: RemarkView[];
|
|
58
|
+
};
|
|
59
|
+
export type ScenarioInfo = {
|
|
60
|
+
name: string;
|
|
61
|
+
expected: string;
|
|
62
|
+
tags?: string[];
|
|
63
|
+
test?: ScenarioTestReference;
|
|
64
|
+
code?: string[];
|
|
65
|
+
};
|
|
66
|
+
export type EvalTimeline = {
|
|
67
|
+
node: string;
|
|
68
|
+
hasEvalFile: boolean;
|
|
69
|
+
scenarios: ScenarioInfo[];
|
|
70
|
+
readings: EvalEntry[];
|
|
71
|
+
retractions: Retraction[];
|
|
72
|
+
dangling: DanglingTrack[];
|
|
73
|
+
};
|
|
74
|
+
export type EvalContext = {
|
|
75
|
+
root: string;
|
|
76
|
+
specs: Awaited<ReturnType<typeof loadSpecs>>;
|
|
77
|
+
idx: DriftIndex;
|
|
78
|
+
hidx: HistoryIndex;
|
|
79
|
+
scidx: ScenarioIndex;
|
|
80
|
+
ynodes: EvalNode[];
|
|
81
|
+
remarks: Map<string, RemarkTrack>;
|
|
82
|
+
};
|
|
83
|
+
export declare function evalContext(root: string, specs: Awaited<ReturnType<typeof loadSpecs>>, idx: DriftIndex, hidx: HistoryIndex, remarks?: Map<string, RemarkTrack>, ynodes?: EvalNode[]): Promise<EvalContext>;
|
|
84
|
+
export declare function evalTimelines(ids: readonly string[], ctx?: EvalContext, opts?: {
|
|
85
|
+
order?: boolean;
|
|
86
|
+
}): Promise<EvalTimeline[]>;
|
|
87
|
+
export declare function evalTimeline(id: string, ctx?: EvalContext): Promise<EvalTimeline>;
|
|
88
|
+
export type BlobResult = {
|
|
89
|
+
ok: true;
|
|
90
|
+
bytes: Buffer;
|
|
91
|
+
mime: string;
|
|
92
|
+
} | {
|
|
93
|
+
ok: false;
|
|
94
|
+
reason: 'invalid' | 'miss';
|
|
95
|
+
message: string;
|
|
96
|
+
};
|
|
97
|
+
export declare function readBlobByHash(hash: string, dir?: string): BlobResult;
|
|
98
|
+
export declare function sniffBlobMime(b: Buffer): string;
|
package/dist/evaltab.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { relative, dirname } from 'node:path';
|
|
2
|
+
import { repoRoot, driftIndex, historyIndex, commitReachable } from '@spexcode/spec-core';
|
|
3
|
+
import { loadSpecs } from '@spexcode/spec-core';
|
|
4
|
+
import { evalRemarkTracks, trackKey } from './host.js';
|
|
5
|
+
import { evalNodes, scenarioCodeAxis } from './scenarios.js';
|
|
6
|
+
import { readSidecar, applyRetractions, evidenceOf, isJsonBlob, humanOkFor } from './sidecar.js';
|
|
7
|
+
import { staleAxes, codeDrift, contentProbeFor, anchorProbeFor } from './freshness.js';
|
|
8
|
+
import { scenarioIndex } from './scenariofresh.js';
|
|
9
|
+
import { hasBlob, getBlob, MISS_BLOB } from './cache.js';
|
|
10
|
+
function toRemarkView(rm, threadId, dangling) {
|
|
11
|
+
return {
|
|
12
|
+
rid: rm.rid,
|
|
13
|
+
ref: `${threadId}#${rm.rid}`,
|
|
14
|
+
by: rm.by, at: rm.at, body: rm.body,
|
|
15
|
+
targetCodeSha: rm.targetCodeSha ?? '',
|
|
16
|
+
resolved: !!rm.resolved,
|
|
17
|
+
...(rm.resolvedAt ? { resolvedAt: rm.resolvedAt } : {}),
|
|
18
|
+
...(rm.resolvedBy ? { resolvedBy: rm.resolvedBy } : {}),
|
|
19
|
+
dangling,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export async function evalContext(root, specs, idx, hidx, remarks, ynodes) {
|
|
23
|
+
const nodes = ynodes ?? evalNodes(root);
|
|
24
|
+
const scidx = await scenarioIndex(root, nodes.map((n) => n.evalPath));
|
|
25
|
+
return { root, specs, idx, hidx, scidx, ynodes: nodes, remarks: remarks ?? evalRemarkTracks() };
|
|
26
|
+
}
|
|
27
|
+
// @@@ one read, one batch - the freshness engine's Git work is immutable-object work, so it is owned ONCE
|
|
28
|
+
// per read rather than once per reading. evalTimelines is the plural the graph build actually wants: it
|
|
29
|
+
// plans every node's rows first (pure fs + in-memory projection), primes the content and anchor probes with
|
|
30
|
+
// the WHOLE demand set, then assembles. Singular evalTimeline is the one-id case of the same path.
|
|
31
|
+
// @@@ `order` skips the FRESHNESS pass, not the rows - a caller that only needs to know WHICH scenarios are
|
|
32
|
+
// in play and in what sequence (identity, filed time, measured-or-blind) pays for none of the probes: on a
|
|
33
|
+
// 415-node scope the freshness pass is 25s and 1476 git children, the rows themselves are milliseconds.
|
|
34
|
+
// Rows returned this way carry `freshnessDeferred` INSTEAD of `fresh`/`staleAxes`, and evalReviewState
|
|
35
|
+
// refuses them loudly — an order row can never be mistaken for a measured verdict.
|
|
36
|
+
export async function evalTimelines(ids, ctx, opts = {}) {
|
|
37
|
+
const root = ctx?.root ?? repoRoot();
|
|
38
|
+
const ynodes = ctx?.ynodes ?? evalNodes(root);
|
|
39
|
+
const specs = ctx?.specs ?? await loadSpecs();
|
|
40
|
+
const idx = ctx?.idx ?? await driftIndex(root);
|
|
41
|
+
const hidx = ctx?.hidx ?? await historyIndex(root);
|
|
42
|
+
const scidx = ctx?.scidx ?? await scenarioIndex(root, ynodes.map((n) => n.evalPath));
|
|
43
|
+
const tracks = ctx?.remarks ?? evalRemarkTracks();
|
|
44
|
+
const probe = contentProbeFor(root);
|
|
45
|
+
const anchors = anchorProbeFor(root, idx);
|
|
46
|
+
const plans = ids.map((id) => {
|
|
47
|
+
const ynode = ynodes.find((n) => n.id === id);
|
|
48
|
+
if (!ynode)
|
|
49
|
+
return { id, codeEntries: [], rows: [], retractions: [], oks: [] };
|
|
50
|
+
const codeEntries = specs.find((s) => dirname(s.path) === relative(root, ynode.dir))?.codeEntries ?? [];
|
|
51
|
+
const byName = new Map(ynode.scenarios.map((s) => [s.name, s]));
|
|
52
|
+
const { readings, retractions, oks } = readSidecar(ynode.sidecarPath);
|
|
53
|
+
const rows = applyRetractions(readings, retractions).map((reading) => ({
|
|
54
|
+
reading, axis: scenarioCodeAxis(byName.get(reading.scenario)?.code, codeEntries),
|
|
55
|
+
}));
|
|
56
|
+
return { id, ynode, codeEntries, rows, retractions, oks };
|
|
57
|
+
});
|
|
58
|
+
// an order-only read stops HERE, before a single probe: the rows above are already the whole answer.
|
|
59
|
+
if (opts.order)
|
|
60
|
+
return plans.map((plan) => assembleTimeline(plan.id, plan.ynode, plan.rows, plan.retractions, plan.oks, {
|
|
61
|
+
idx, scidx, tracks, probe, anchors, order: true,
|
|
62
|
+
}));
|
|
63
|
+
// An off-history anchor is the only reading that needs a content verdict. Plan the population first so the
|
|
64
|
+
// probe can answer many anchor/HEAD pairs through bounded plural children instead of one child per anchor.
|
|
65
|
+
const contentDemands = plans.flatMap((plan) => plan.ynode
|
|
66
|
+
? plan.rows.filter((row) => !commitReachable(idx, row.reading.codeSha))
|
|
67
|
+
.map((row) => ({ anchorSha: row.reading.codeSha, paths: row.axis.paths, evalPath: plan.ynode.evalPath }))
|
|
68
|
+
: []);
|
|
69
|
+
if (probe.primeMany)
|
|
70
|
+
await probe.primeMany(contentDemands);
|
|
71
|
+
else
|
|
72
|
+
await Promise.all(contentDemands.map((row) => probe.prime?.(row.anchorSha, [...row.paths], row.evalPath)));
|
|
73
|
+
await anchors.prime?.(plans.flatMap((plan) => plan.rows.map((row) => ({ sinceSha: row.reading.codeSha, entries: row.axis.entries }))));
|
|
74
|
+
// the primes above only RECORDED which eval.md blocks the assemble step will compare; one batched pair of
|
|
75
|
+
// children answers the whole read's worth, the same shape the anchor prime above already has.
|
|
76
|
+
await probe.primeBlocks?.();
|
|
77
|
+
return plans.map((plan) => assembleTimeline(plan.id, plan.ynode, plan.rows, plan.retractions, plan.oks, {
|
|
78
|
+
idx, scidx, tracks, probe, anchors,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
export async function evalTimeline(id, ctx) {
|
|
82
|
+
return (await evalTimelines([id], ctx))[0];
|
|
83
|
+
}
|
|
84
|
+
function assembleTimeline(id, ynode, rows, retractions, oks, { idx, scidx, tracks, probe, anchors, order }) {
|
|
85
|
+
if (!ynode)
|
|
86
|
+
return { node: id, hasEvalFile: false, scenarios: [], readings: [], retractions: [], dangling: [] };
|
|
87
|
+
const byName = new Map(ynode.scenarios.map((s) => [s.name, s]));
|
|
88
|
+
const remarksFor = (scenario) => tracks.get(trackKey(id, scenario))?.remarks ?? [];
|
|
89
|
+
const threadFor = (scenario) => tracks.get(trackKey(id, scenario))?.thread;
|
|
90
|
+
const scenarios = ynode.scenarios.map((s) => ({
|
|
91
|
+
name: s.name, expected: s.expected,
|
|
92
|
+
...(s.tags?.length ? { tags: s.tags } : {}), ...(s.test ? { test: s.test } : {}),
|
|
93
|
+
...(s.code?.length ? { code: s.code } : {}),
|
|
94
|
+
}));
|
|
95
|
+
const readings = [];
|
|
96
|
+
for (const { reading: r, axis } of rows) {
|
|
97
|
+
const sc = byName.get(r.scenario);
|
|
98
|
+
const axes = order ? [] : staleAxes(r, axis.entries, ynode.evalPath, idx, scidx, remarksFor(r.scenario).map((rm) => ({ resolved: !!rm.resolved, resolvedAt: rm.resolvedAt })), probe, sc, anchors);
|
|
99
|
+
const drift = !order && axes.includes('code') ? codeDrift(idx, r.codeSha, axis.entries, probe) : [];
|
|
100
|
+
const evidence = evidenceOf(r).map((e) => ({ hash: e.hash, kind: e.kind, state: hasBlob(e.hash) ? 'present' : 'miss' }));
|
|
101
|
+
const primary = evidence.find((e) => e.kind === 'video') ?? evidence[0];
|
|
102
|
+
const okRow = humanOkFor(oks, r.scenario, r.ts);
|
|
103
|
+
readings.push({
|
|
104
|
+
scenario: r.scenario,
|
|
105
|
+
expected: byName.get(r.scenario)?.expected ?? '',
|
|
106
|
+
codeSha: r.codeSha,
|
|
107
|
+
...(evidence.length ? { evidence } : {}),
|
|
108
|
+
blob: primary?.hash ?? null,
|
|
109
|
+
...(primary ? { blobKind: primary.kind } : {}),
|
|
110
|
+
...(r.timelineBlob ? { timelineBlob: r.timelineBlob } : {}),
|
|
111
|
+
...(r.evaluator ? { evaluator: r.evaluator } : {}),
|
|
112
|
+
...(r.by ? { by: r.by } : {}),
|
|
113
|
+
...(r.verdict ? { verdict: r.verdict } : {}),
|
|
114
|
+
ts: r.ts,
|
|
115
|
+
fresh: !order && axes.length === 0,
|
|
116
|
+
staleAxes: axes,
|
|
117
|
+
...(order ? { freshnessDeferred: true } : {}),
|
|
118
|
+
...(drift.length ? { codeDrift: drift } : {}),
|
|
119
|
+
blobState: primary ? primary.state : 'none',
|
|
120
|
+
...(threadFor(r.scenario) ? { thread: threadFor(r.scenario) } : {}),
|
|
121
|
+
...(okRow ? { humanOk: { by: okRow.by, ts: okRow.ts } } : {}),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
readings.reverse();
|
|
125
|
+
const declared = new Set(ynode.scenarios.map((s) => s.name));
|
|
126
|
+
const dangling = [];
|
|
127
|
+
for (const [, track] of tracks) {
|
|
128
|
+
if (track.node !== id || !track.remarks.length)
|
|
129
|
+
continue;
|
|
130
|
+
const hosts = readings.filter((r) => r.scenario === track.scenario);
|
|
131
|
+
if (!hosts.length) {
|
|
132
|
+
if (!declared.has(track.scenario)) {
|
|
133
|
+
dangling.push({
|
|
134
|
+
scenario: track.scenario, threadId: track.threadId, thread: track.thread,
|
|
135
|
+
remarks: track.remarks.map((rm) => toRemarkView(rm, track.threadId, true)),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const latest = hosts[0];
|
|
141
|
+
for (const rm of track.remarks) {
|
|
142
|
+
const target = hosts.find((r) => r.codeSha === rm.targetCodeSha);
|
|
143
|
+
const host = target ?? latest;
|
|
144
|
+
(host.remarks ??= []).push(toRemarkView(rm, track.threadId, !target));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { node: id, hasEvalFile: true, scenarios, readings, retractions: [...retractions].reverse(), dangling };
|
|
148
|
+
}
|
|
149
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
150
|
+
export function readBlobByHash(hash, dir) {
|
|
151
|
+
if (!HEX64.test(hash))
|
|
152
|
+
return { ok: false, reason: 'invalid', message: 'bad evidence hash' };
|
|
153
|
+
const bytes = getBlob(hash, dir); // undefined dir → the live cache (cache.ts default); a temp dir in tests
|
|
154
|
+
if (!bytes)
|
|
155
|
+
return { ok: false, reason: 'miss', message: MISS_BLOB };
|
|
156
|
+
return { ok: true, bytes, mime: sniffBlobMime(bytes) };
|
|
157
|
+
}
|
|
158
|
+
export function sniffBlobMime(b) {
|
|
159
|
+
if (b.length >= 4 && b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47)
|
|
160
|
+
return 'image/png';
|
|
161
|
+
if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
|
|
162
|
+
return 'image/jpeg';
|
|
163
|
+
if (b.length >= 4 && b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46)
|
|
164
|
+
return 'image/gif';
|
|
165
|
+
// WebM/Matroska begin with the EBML magic 1A 45 DF A3; disambiguate from a RIFF/WEBP image above.
|
|
166
|
+
if (b.length >= 4 && b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3)
|
|
167
|
+
return 'video/webm';
|
|
168
|
+
// ISO-BMFF (MP4/MOV): a `ftyp` box type at bytes 4..8, after its 4-byte size.
|
|
169
|
+
if (b.length >= 12 && b.toString('ascii', 4, 8) === 'ftyp')
|
|
170
|
+
return 'video/mp4';
|
|
171
|
+
if (b.length >= 12 && b.toString('ascii', 0, 4) === 'RIFF' && b.toString('ascii', 8, 12) === 'WEBP')
|
|
172
|
+
return 'image/webp';
|
|
173
|
+
if (b.length && !b.includes(0))
|
|
174
|
+
return isJsonBlob(b) ? 'application/json' : 'text/plain; charset=utf-8';
|
|
175
|
+
return 'application/octet-stream';
|
|
176
|
+
}
|
package/dist/filing.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Reading } from './sidecar.js';
|
|
2
|
+
export type FileResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
reading: Reading;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
error: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function fileHumanReading(nodeId: string, input: {
|
|
10
|
+
scenario: string;
|
|
11
|
+
status: 'pass' | 'fail';
|
|
12
|
+
note?: string;
|
|
13
|
+
transcript?: string;
|
|
14
|
+
by?: string;
|
|
15
|
+
}): FileResult;
|
|
16
|
+
export declare function evalReadingFiler(nodeId: string, scenario: string, root?: string): string | null;
|
package/dist/filing.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { repoRoot, headSha } from '@spexcode/spec-core';
|
|
2
|
+
import { evalNodes, resolveEvalNode, scenarioHash } from './scenarios.js';
|
|
3
|
+
import { appendReading, readReadings, isJsonBlob } from './sidecar.js';
|
|
4
|
+
import { putBlob } from './cache.js';
|
|
5
|
+
export function fileHumanReading(nodeId, input) {
|
|
6
|
+
const root = repoRoot();
|
|
7
|
+
const res = resolveEvalNode(evalNodes(root), nodeId);
|
|
8
|
+
if (!res.ok)
|
|
9
|
+
return { ok: false, error: res.error };
|
|
10
|
+
const node = res.node;
|
|
11
|
+
const sc = node.scenarios.find((s) => s.name === input.scenario);
|
|
12
|
+
if (!sc)
|
|
13
|
+
return { ok: false, error: `'${nodeId}' has no scenario '${input.scenario}'` };
|
|
14
|
+
if (input.status !== 'pass' && input.status !== 'fail')
|
|
15
|
+
return { ok: false, error: 'status must be pass or fail' };
|
|
16
|
+
const buf = input.transcript ? Buffer.from(input.transcript) : null;
|
|
17
|
+
const blob = buf ? putBlob(buf) : null;
|
|
18
|
+
const reading = {
|
|
19
|
+
scenario: sc.name,
|
|
20
|
+
codeSha: headSha(root),
|
|
21
|
+
scenarioHash: scenarioHash(sc),
|
|
22
|
+
...(blob ? { evidence: [{ hash: blob, kind: (buf && isJsonBlob(buf) ? 'data' : 'transcript') }] } : {}),
|
|
23
|
+
...(input.by ? { by: input.by } : {}),
|
|
24
|
+
verdict: { status: input.status, ...(input.note ? { note: input.note } : {}) },
|
|
25
|
+
ts: new Date().toISOString(),
|
|
26
|
+
};
|
|
27
|
+
appendReading(node.sidecarPath, reading);
|
|
28
|
+
return { ok: true, reading };
|
|
29
|
+
}
|
|
30
|
+
export function evalReadingFiler(nodeId, scenario, root = repoRoot()) {
|
|
31
|
+
const res = resolveEvalNode(evalNodes(root), nodeId);
|
|
32
|
+
if (!res.ok)
|
|
33
|
+
return null;
|
|
34
|
+
const forScenario = readReadings(res.node.sidecarPath).filter((r) => r.scenario === scenario);
|
|
35
|
+
return forScenario.length ? forScenario[forScenario.length - 1].by ?? null : null;
|
|
36
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type DriftIndex } from '@spexcode/spec-core';
|
|
2
|
+
import { type RelationEntry } from '@spexcode/spec-core';
|
|
3
|
+
import type { Reading } from './sidecar.js';
|
|
4
|
+
import { type Scenario, type ScenarioCodeAxisSource } from './scenarios.js';
|
|
5
|
+
import { type ScenarioIndex } from './scenariofresh.js';
|
|
6
|
+
export type StaleAxis = 'code' | 'scenario' | 'remark' | 'anchor';
|
|
7
|
+
export type ContentProbeDemand = {
|
|
8
|
+
anchorSha: string;
|
|
9
|
+
paths: readonly string[];
|
|
10
|
+
evalPath: string;
|
|
11
|
+
};
|
|
12
|
+
export type ContentProbe = {
|
|
13
|
+
changed(anchorSha: string, path: string): boolean | null;
|
|
14
|
+
canTestify(anchorSha: string): boolean;
|
|
15
|
+
scenarioDiffers(anchorSha: string, evalPath: string, scenario: string): boolean;
|
|
16
|
+
inAnchorPast(anchorSha: string, commit: string): boolean;
|
|
17
|
+
prime?(anchorSha: string, paths: string[], evalPath: string): Promise<void>;
|
|
18
|
+
primeMany?(demands: readonly ContentProbeDemand[]): Promise<void>;
|
|
19
|
+
primeBlocks?(): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
export declare function freshnessCacheSize(root?: string): number;
|
|
22
|
+
export declare function anchorVerdictCacheSize(root?: string): number;
|
|
23
|
+
export declare function contentProbeFor(root: string): ContentProbe;
|
|
24
|
+
export type AnchorProbe = {
|
|
25
|
+
hit(sinceSha: string, path: string, selectors: readonly string[]): boolean | null;
|
|
26
|
+
prime?(demands: readonly AnchorDemand[]): Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
export type AnchorDemand = {
|
|
29
|
+
sinceSha: string;
|
|
30
|
+
entries: readonly RelationEntry[];
|
|
31
|
+
};
|
|
32
|
+
export declare function anchorProbeFor(root: string, idx: DriftIndex): AnchorProbe;
|
|
33
|
+
export declare function anchorProblems(root: string, entries: readonly RelationEntry[]): string[];
|
|
34
|
+
export type RemarkSignal = {
|
|
35
|
+
resolved: boolean;
|
|
36
|
+
resolvedAt?: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function remarkStale(reading: {
|
|
39
|
+
ts: string;
|
|
40
|
+
}, remarks: RemarkSignal[]): boolean;
|
|
41
|
+
export declare function changedSince(idx: DriftIndex, sinceSha: string, path: string, probe?: ContentProbe): boolean;
|
|
42
|
+
export declare function codeDrift(idx: DriftIndex, sinceSha: string, codeAxis: ScenarioCodeAxisSource, probe?: ContentProbe): {
|
|
43
|
+
file: string;
|
|
44
|
+
behind: number;
|
|
45
|
+
}[];
|
|
46
|
+
export declare function staleAxes(reading: Reading, codeAxis: ScenarioCodeAxisSource, evalPath: string, didx: DriftIndex, scIdx: ScenarioIndex, remarks?: RemarkSignal[], probe?: ContentProbe, current?: Scenario, // the scenario's CURRENT declaration (undefined = gone from eval.md) — the hash compare's other side
|
|
47
|
+
anchors?: AnchorProbe): StaleAxis[];
|
|
48
|
+
export declare function isStale(reading: Reading, codeAxis: ScenarioCodeAxisSource, evalPath: string, didx: DriftIndex, scIdx: ScenarioIndex, remarks?: RemarkSignal[], probe?: ContentProbe, current?: Scenario, anchors?: AnchorProbe): boolean;
|