@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.
@@ -0,0 +1,55 @@
1
+ export type Verdict = {
2
+ status: 'pass' | 'fail';
3
+ note?: string;
4
+ };
5
+ export type EvidenceKind = 'image' | 'transcript' | 'video' | 'data';
6
+ export type Evidence = {
7
+ hash: string;
8
+ kind: EvidenceKind;
9
+ };
10
+ export type Reading = {
11
+ scenario: string;
12
+ codeSha: string;
13
+ scenarioHash?: string;
14
+ evidence?: Evidence[];
15
+ blob?: string | null;
16
+ blobKind?: EvidenceKind;
17
+ timelineBlob?: string;
18
+ evaluator?: string;
19
+ by?: string;
20
+ verdict?: Verdict;
21
+ ts: string;
22
+ };
23
+ export declare function evidenceOf(r: {
24
+ evidence?: Evidence[];
25
+ blob?: string | null;
26
+ blobKind?: EvidenceKind;
27
+ }): Evidence[];
28
+ export declare function isJsonBlob(b: Buffer): boolean;
29
+ export type Retraction = {
30
+ retracts: string;
31
+ scenario: string;
32
+ note?: string;
33
+ by?: string;
34
+ ts: string;
35
+ };
36
+ export type HumanOk = {
37
+ kind: 'human-ok';
38
+ scenario: string;
39
+ okTs: string;
40
+ okSha: string;
41
+ by: string;
42
+ ts: string;
43
+ };
44
+ export declare function readSidecar(sidecarPath: string): {
45
+ readings: Reading[];
46
+ retractions: Retraction[];
47
+ oks: HumanOk[];
48
+ };
49
+ export declare function applyRetractions(readings: Reading[], retractions: Retraction[]): Reading[];
50
+ export declare function readReadings(sidecarPath: string): Reading[];
51
+ export declare function appendReading(sidecarPath: string, r: Reading): void;
52
+ export declare function appendRetraction(sidecarPath: string, r: Retraction): void;
53
+ export declare function appendHumanOk(sidecarPath: string, r: HumanOk): void;
54
+ export declare function humanOkFor(oks: HumanOk[], scenario: string, readingTs: string): HumanOk | null;
55
+ export declare function latestPerScenario(readings: Reading[]): Map<string, Reading>;
@@ -0,0 +1,82 @@
1
+ import { readFileSync, appendFileSync, existsSync } from 'node:fs';
2
+ export function evidenceOf(r) {
3
+ if (r.evidence?.length)
4
+ return r.evidence;
5
+ if (r.blob)
6
+ return [{ hash: r.blob, kind: r.blobKind ?? 'image' }];
7
+ return [];
8
+ }
9
+ export function isJsonBlob(b) {
10
+ if (!b.length || b.includes(0))
11
+ return false; // empty or binary → not JSON text
12
+ if (b.length > 4_000_000)
13
+ return false; // don't parse an unbounded blob just to sniff a type
14
+ const s = b.toString('utf8').trim();
15
+ const open = s[0], close = s[s.length - 1];
16
+ if (!((open === '{' && close === '}') || (open === '[' && close === ']')))
17
+ return false;
18
+ try {
19
+ const v = JSON.parse(s);
20
+ return v !== null && typeof v === 'object';
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ export function readSidecar(sidecarPath) {
27
+ const readings = [];
28
+ const retractions = [];
29
+ const oks = [];
30
+ if (!existsSync(sidecarPath))
31
+ return { readings, retractions, oks };
32
+ for (const line of readFileSync(sidecarPath, 'utf8').split('\n')) {
33
+ const t = line.trim();
34
+ if (!t)
35
+ continue;
36
+ try {
37
+ const r = JSON.parse(t);
38
+ if (!r || typeof r.scenario !== 'string')
39
+ continue;
40
+ if (typeof r.retracts === 'string')
41
+ retractions.push(r);
42
+ else if (r.kind === 'human-ok' && typeof r.okTs === 'string')
43
+ oks.push(r);
44
+ else if (typeof r.codeSha === 'string')
45
+ readings.push(r);
46
+ }
47
+ catch { /* Keep earlier events readable after an incomplete append. */ }
48
+ }
49
+ return { readings, retractions, oks };
50
+ }
51
+ export function applyRetractions(readings, retractions) {
52
+ if (!retractions.length)
53
+ return readings;
54
+ const gone = new Set(retractions.map((x) => `${x.scenario}\0${x.retracts}`));
55
+ return readings.filter((r) => !gone.has(`${r.scenario}\0${r.ts}`));
56
+ }
57
+ export function readReadings(sidecarPath) {
58
+ const { readings, retractions } = readSidecar(sidecarPath);
59
+ return applyRetractions(readings, retractions);
60
+ }
61
+ export function appendReading(sidecarPath, r) {
62
+ appendFileSync(sidecarPath, JSON.stringify(r) + '\n');
63
+ }
64
+ export function appendRetraction(sidecarPath, r) {
65
+ appendFileSync(sidecarPath, JSON.stringify(r) + '\n');
66
+ }
67
+ export function appendHumanOk(sidecarPath, r) {
68
+ appendFileSync(sidecarPath, JSON.stringify(r) + '\n');
69
+ }
70
+ export function humanOkFor(oks, scenario, readingTs) {
71
+ let hit = null;
72
+ for (const o of oks)
73
+ if (o.scenario === scenario && o.okTs === readingTs)
74
+ hit = o;
75
+ return hit;
76
+ }
77
+ export function latestPerScenario(readings) {
78
+ const m = new Map();
79
+ for (const r of readings)
80
+ m.set(r.scenario, r); // later lines overwrite earlier → last wins
81
+ return m;
82
+ }
@@ -0,0 +1,25 @@
1
+ export type TimelineEvent = {
2
+ at: number;
3
+ step: string;
4
+ node?: string;
5
+ };
6
+ export type StepTimeline = {
7
+ v: 2;
8
+ axis: string;
9
+ events: TimelineEvent[];
10
+ };
11
+ export type LegacyTimelineEvent = {
12
+ tMs: number;
13
+ step: string;
14
+ node?: string;
15
+ };
16
+ export type LegacyStepTimeline = {
17
+ v: 1;
18
+ events: LegacyTimelineEvent[];
19
+ };
20
+ export declare function validateTimeline(raw: unknown): string[];
21
+ export declare function normalizeTimeline(raw: unknown): {
22
+ axis: string;
23
+ events: TimelineEvent[];
24
+ };
25
+ export declare function stepAt(events: TimelineEvent[], pos: number): TimelineEvent | null;
@@ -0,0 +1,65 @@
1
+ const V2_EVENT_KEYS = new Set(['at', 'step', 'node']);
2
+ const V1_EVENT_KEYS = new Set(['tMs', 'step', 'node']);
3
+ export function validateTimeline(raw) {
4
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
5
+ return ['timeline must be a JSON object { v, axis, events }'];
6
+ const o = raw;
7
+ if (o.v !== 1 && o.v !== 2)
8
+ return ['`v` must be 1 (legacy time axis) or 2 (axis-tagged)'];
9
+ const errs = [];
10
+ const v2 = o.v === 2;
11
+ const posKey = v2 ? 'at' : 'tMs';
12
+ const rootKeys = v2 ? new Set(['v', 'axis', 'events']) : new Set(['v', 'events']);
13
+ const evKeys = v2 ? V2_EVENT_KEYS : V1_EVENT_KEYS;
14
+ for (const k of Object.keys(o))
15
+ if (!rootKeys.has(k))
16
+ errs.push(`unknown field \`${k}\` (allowed: ${[...rootKeys].join(', ')})`);
17
+ if (v2 && (typeof o.axis !== 'string' || !o.axis.trim()))
18
+ errs.push('`axis` must be a non-empty string (e.g. time, frame, line, index)');
19
+ if (!Array.isArray(o.events)) {
20
+ errs.push('`events` must be an array');
21
+ return errs;
22
+ }
23
+ let prev = -Infinity;
24
+ o.events.forEach((e, i) => {
25
+ if (typeof e !== 'object' || e === null || Array.isArray(e)) {
26
+ errs.push(`events[${i}] must be an object`);
27
+ return;
28
+ }
29
+ const ev = e;
30
+ for (const k of Object.keys(ev))
31
+ if (!evKeys.has(k))
32
+ errs.push(`events[${i}]: unknown field \`${k}\` (allowed: ${[...evKeys].join(', ')})`);
33
+ const pos = ev[posKey];
34
+ if (typeof pos !== 'number' || !Number.isFinite(pos) || pos < 0) {
35
+ errs.push(`events[${i}].${posKey} must be a finite number ≥ 0`);
36
+ }
37
+ else {
38
+ if (pos < prev)
39
+ errs.push(`events[${i}].${posKey} is out of order (the list is ordered by position)`);
40
+ prev = pos;
41
+ }
42
+ if (typeof ev.step !== 'string' || !ev.step.trim())
43
+ errs.push(`events[${i}].step must be a non-empty string`);
44
+ if (ev.node !== undefined && (typeof ev.node !== 'string' || !ev.node.trim()))
45
+ errs.push(`events[${i}].node must be a non-empty string when present`);
46
+ });
47
+ return errs;
48
+ }
49
+ export function normalizeTimeline(raw) {
50
+ const o = (raw ?? {});
51
+ const events = Array.isArray(o.events) ? o.events : [];
52
+ if (o.v === 1)
53
+ return { axis: 'time', events: events.map((e) => ({ at: e.tMs, step: e.step, ...(e.node ? { node: e.node } : {}) })) };
54
+ return { axis: typeof o.axis === 'string' ? o.axis : 'time', events: events.map((e) => ({ at: e.at, step: e.step, ...(e.node ? { node: e.node } : {}) })) };
55
+ }
56
+ export function stepAt(events, pos) {
57
+ let hit = null;
58
+ for (const e of events) {
59
+ if (e.at <= pos)
60
+ hit = e;
61
+ else
62
+ break;
63
+ }
64
+ return hit;
65
+ }
@@ -0,0 +1 @@
1
+ export declare const isUiPath: (p: string) => boolean;
@@ -0,0 +1,2 @@
1
+ const UI_FILE = /\.(jsx|tsx|vue|svelte|css)$/;
2
+ export const isUiPath = (p) => UI_FILE.test(p) || p.includes('spec-dashboard/');
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@spexcode/spec-eval",
3
+ "version": "0.6.5",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./dist/index.js",
7
+ "./sessioneval": "./dist/sessioneval.js",
8
+ "./evaltab": "./dist/evaltab.js",
9
+ "./cli": "./dist/cli.js",
10
+ "./cache": "./dist/cache.js",
11
+ "./filing": "./dist/filing.js",
12
+ "./humanok": "./dist/humanok.js",
13
+ "./scenarios": "./dist/scenarios.js",
14
+ "./host": "./dist/host.js",
15
+ "./remarks": "./dist/remarks.js",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": ["dist"],
19
+ "description": "The eval/loss engine \u2014 eval.md declares scenarios, a flat evals.ndjson sidecar records readings (a second git-as-database axis), freshness compares each reading's stored scenario-contract hash and git ancestry on its governed code, and evidence lives content-addressed under the shared git common dir. `spex eval add|ls|scenario ls|lint|retract|clean`.",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "node ../scripts/build-dist.mjs",
25
+ "prepublishOnly": "node ../scripts/release-publish.mjs --from-package-publish",
26
+ "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
27
+ },
28
+ "dependencies": {
29
+ "@spexcode/spec-core": "0.6.5"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20.16.0",
33
+ "tsx": "^4.19.2",
34
+ "typescript": "^5.6.3"
35
+ }
36
+ }