spexcode 0.2.6 → 0.2.7
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/README.md +4 -2
- package/package.json +1 -1
- package/spec-cli/src/cli.ts +9 -1
- package/spec-cli/src/guide.ts +23 -16
- package/spec-cli/src/harness.ts +18 -9
- package/spec-cli/src/help.ts +4 -1
- package/spec-cli/src/sessions.ts +34 -15
- package/spec-cli/templates/spec/project/.config/supervisor/spec.md +1 -1
- package/spec-dashboard/dist/assets/{Dashboard-BlRRsxE7.js → Dashboard-P0B9ukSG.js} +3 -3
- package/spec-dashboard/dist/assets/{EvalsPage-BzVE38-Z.js → EvalsPage-BrvAGyc4.js} +1 -1
- package/spec-dashboard/dist/assets/{FoldToggle-DFuLVOeu.js → FoldToggle-BuQ0lokE.js} +1 -1
- package/spec-dashboard/dist/assets/{IssuesPage-CzDaazhe.js → IssuesPage-H-D8aHEl.js} +1 -1
- package/spec-dashboard/dist/assets/{MobileApp-CXQrQCNp.js → MobileApp-oZXIeCPb.js} +1 -1
- package/spec-dashboard/dist/assets/{SessionInterface-D1pUBl6q.js → SessionInterface-Blr_MEdU.js} +1 -1
- package/spec-dashboard/dist/assets/{SessionWindow-Y25Bwg1e.js → SessionWindow-LcCzBMU7.js} +2 -2
- package/spec-dashboard/dist/assets/{Settings-R610Vbzd.js → Settings-_yOye-In.js} +1 -1
- package/spec-dashboard/dist/assets/{index-BO0Zuweu.js → index-BhIslAau.js} +2 -2
- package/spec-dashboard/dist/index.html +1 -1
- package/spec-yatsu/src/cli.ts +3 -5
- package/spec-yatsu/src/evaltab.ts +3 -2
- package/spec-yatsu/src/filing.ts +3 -4
- package/spec-yatsu/src/freshness.ts +1 -3
- package/spec-yatsu/src/proof.ts +3 -2
- package/spec-yatsu/src/scenariofresh.ts +27 -5
- package/spec-yatsu/src/sidecar.ts +8 -8
- package/spec-yatsu/src/evaluator.ts +0 -24
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
} catch (e) { /* localStorage/matchMedia unavailable — default (light) stands */ }
|
|
20
20
|
})()
|
|
21
21
|
</script>
|
|
22
|
-
<script type="module" crossorigin src="/assets/index-
|
|
22
|
+
<script type="module" crossorigin src="/assets/index-BhIslAau.js"></script>
|
|
23
23
|
<link rel="stylesheet" crossorigin href="/assets/index-uGs9v_9o.css">
|
|
24
24
|
</head>
|
|
25
25
|
<body>
|
package/spec-yatsu/src/cli.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { staleAxes, contentProbeFor } from './freshness.js'
|
|
|
10
10
|
import { scenarioIndex } from './scenariofresh.js'
|
|
11
11
|
import { loadEvalRemarkTracks, trackKey } from '../../spec-cli/src/issues.js'
|
|
12
12
|
import { stripRefSigil } from '../../spec-cli/src/mentions.js'
|
|
13
|
-
import { evaluatorTag } from './evaluator.js'
|
|
14
13
|
import { putBlob, blobPath, listBlobs, gc, isStrayBlob } from './cache.js'
|
|
15
14
|
import { validateTimeline, normalizeTimeline } from './timeline.js'
|
|
16
15
|
import { evalTimeline, readBlobByHash, type EvalTimeline } from './evaltab.js'
|
|
@@ -303,7 +302,6 @@ async function evalCmd(args: string[]): Promise<number> {
|
|
|
303
302
|
codeSha: headSha(root),
|
|
304
303
|
...(evidence.length ? { evidence } : {}),
|
|
305
304
|
...(timelineBlob ? { timelineBlob } : {}),
|
|
306
|
-
evaluator: evaluatorTag(),
|
|
307
305
|
// the filing session — the originator an eval-comment thread loops in ([[mentions]]); absent if unknown
|
|
308
306
|
...((envSessionId() ?? undefined) ? { by: envSessionId()! } : {}),
|
|
309
307
|
verdict,
|
|
@@ -313,7 +311,7 @@ async function evalCmd(args: string[]): Promise<number> {
|
|
|
313
311
|
const ev = evidence.length
|
|
314
312
|
? evidence.map((e) => `${e.kind} ${e.hash.slice(0, 12)}…`).join(', ') + (timelineBlob ? ' +timeline' : '')
|
|
315
313
|
: 'no evidence'
|
|
316
|
-
console.log(` ✓ '${id}' scenario '${scenario.name}' → ${verdictText(verdict)} @ ${reading.codeSha.slice(0, 7)}
|
|
314
|
+
console.log(` ✓ '${id}' scenario '${scenario.name}' → ${verdictText(verdict)} @ ${reading.codeSha.slice(0, 7)} (${ev})`)
|
|
317
315
|
console.log(`spex yatsu eval: 1 measurement filed`)
|
|
318
316
|
|
|
319
317
|
// @@@mis-anchor guard - a codeSha names a COMMIT, never a working tree: filed over uncommitted governed
|
|
@@ -423,7 +421,7 @@ async function retractCmd(args: string[]): Promise<number> {
|
|
|
423
421
|
const now = left.length
|
|
424
422
|
? `latest is now ${left[left.length - 1].ts} (${verdictText(left[left.length - 1].verdict)})`
|
|
425
423
|
: 'the scenario is unmeasured again (yatsu-missing)'
|
|
426
|
-
console.log(` ⟲ '${id}' scenario '${scenario}' reading @ ${target.ts} (${verdictText(target.verdict)}
|
|
424
|
+
console.log(` ⟲ '${id}' scenario '${scenario}' reading @ ${target.ts} (${verdictText(target.verdict)}) retracted — ${now}`)
|
|
427
425
|
console.log('spex yatsu retract: 1 reading retracted (an appended event — commit the sidecar so the retraction is attributed)')
|
|
428
426
|
return 0
|
|
429
427
|
}
|
|
@@ -521,7 +519,7 @@ export function formatTimeline(tl: EvalTimeline): string {
|
|
|
521
519
|
const ev = list.length
|
|
522
520
|
? list.map((e) => e.state === 'miss' ? 'miss original file' : `${e.kind} ${(e.hash ?? '').slice(0, 12)}…`).join(', ')
|
|
523
521
|
: 'no evidence'
|
|
524
|
-
const head = ` ${r.scenario.padEnd(w)} ${verdictText(r.verdict)} ${badge} ${r.
|
|
522
|
+
const head = ` ${r.scenario.padEnd(w)} ${verdictText(r.verdict)} ${badge} ${r.codeSha.slice(0, 7)} ${ev} ${r.ts}`
|
|
525
523
|
return r.expected ? [head, ` ${' '.repeat(w)} expected: ${r.expected}`] : [head]
|
|
526
524
|
})
|
|
527
525
|
return [`spex yatsu show: '${tl.node}' — ${tl.readings.length} reading(s), newest first`, '', ...lines, ...retractLines].join('\n')
|
|
@@ -41,7 +41,8 @@ export type EvalEntry = {
|
|
|
41
41
|
blob: string | null
|
|
42
42
|
blobKind?: EvidenceKind
|
|
43
43
|
timelineBlob?: string
|
|
44
|
-
|
|
44
|
+
// legacy instrument tag ('manual@1') — surfaced for old readings only, never written by new filings.
|
|
45
|
+
evaluator?: string
|
|
45
46
|
// the SESSION that filed this reading ([[event-detail]] originator liveness / [[mentions]] loop-in): the
|
|
46
47
|
// reachable actor an un-@'d eval remark courtesy-delivers to (the latest reading's filer is the chain's
|
|
47
48
|
// first link). Surfaced so the eval detail can show whether that session is still alive. Absent on a legacy
|
|
@@ -190,7 +191,7 @@ export async function evalTimeline(id: string, ctx?: EvalContext): Promise<EvalT
|
|
|
190
191
|
blob: primary?.hash ?? null,
|
|
191
192
|
...(primary ? { blobKind: primary.kind } : {}),
|
|
192
193
|
...(r.timelineBlob ? { timelineBlob: r.timelineBlob } : {}),
|
|
193
|
-
evaluator: r.evaluator,
|
|
194
|
+
...(r.evaluator ? { evaluator: r.evaluator } : {}),
|
|
194
195
|
...(r.by ? { by: r.by } : {}),
|
|
195
196
|
...(r.verdict ? { verdict: r.verdict } : {}),
|
|
196
197
|
ts: r.ts,
|
package/spec-yatsu/src/filing.ts
CHANGED
|
@@ -7,9 +7,9 @@ export type FileResult = { ok: true; reading: Reading } | { ok: false; error: st
|
|
|
7
7
|
|
|
8
8
|
// the eval seam over DATA (no argv, no file paths): a caller with a verdict in hand — the HTTP eval route,
|
|
9
9
|
// a programmatic filer — appends through the SAME seam the CLI uses. Optional evidence arrives as text (a
|
|
10
|
-
// report referencing the clip by hash) → a transcript blob in the same content-addressed cache
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// report referencing the clip by hash) → a transcript blob in the same content-addressed cache. yatsu
|
|
11
|
+
// still runs nothing — this only records. The dashboard files nothing through this: [[event-detail]] is
|
|
12
|
+
// read-side on readings.
|
|
13
13
|
export function fileHumanReading(
|
|
14
14
|
nodeId: string,
|
|
15
15
|
input: { scenario: string; status: 'pass' | 'fail'; note?: string; transcript?: string; by?: string },
|
|
@@ -31,7 +31,6 @@ export function fileHumanReading(
|
|
|
31
31
|
scenario: sc.name,
|
|
32
32
|
codeSha: headSha(root),
|
|
33
33
|
...(blob ? { evidence: [{ hash: blob, kind: (buf && isJsonBlob(buf) ? 'data' : 'transcript') as EvidenceKind }] } : {}),
|
|
34
|
-
evaluator: 'manual@1',
|
|
35
34
|
// the filing session (caller-passed — the human annotator has no reachable session, so it stays absent
|
|
36
35
|
// there and the eval-comment loop-in is silent, per [[mentions]])
|
|
37
36
|
...(input.by ? { by: input.by } : {}),
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { git, headSha, ancestorsOf, inAncestors, type DriftIndex } from '../../spec-cli/src/git.js'
|
|
2
2
|
import type { Reading } from './sidecar.js'
|
|
3
|
-
import { isEvaluatorStale } from './evaluator.js'
|
|
4
3
|
import { scenarioChangeCommits, scenarioBlocksAt, type ScenarioIndex } from './scenariofresh.js'
|
|
5
4
|
|
|
6
5
|
// the CODE axis is touch-based (DriftIndex), so a code-file rename is out of scope — the same blind spot lint's code-drift has
|
|
7
6
|
|
|
8
|
-
export type StaleAxis = 'code' | 'scenario' | '
|
|
7
|
+
export type StaleAxis = 'code' | 'scenario' | 'remark' | 'anchor'
|
|
9
8
|
|
|
10
9
|
// @@@ off-history content fallback - ancestry can't testify for a codeSha that isn't reachable from HEAD
|
|
11
10
|
// (fold/rebase/squash-merge/cherry-pick all orphan the anchor), but the TREES still can: while the anchor
|
|
@@ -143,7 +142,6 @@ export function staleAxes(
|
|
|
143
142
|
if (codeFiles.some((f) => changedSince(didx, reading.codeSha, f, probe))) axes.push('code')
|
|
144
143
|
if (scenarioMoved(scIdx, didx, reading.codeSha, yatsuPath, reading.scenario, probe)) axes.push('scenario')
|
|
145
144
|
}
|
|
146
|
-
if (isEvaluatorStale(reading.evaluator)) axes.push('evaluator')
|
|
147
145
|
if (remarkStale(reading, remarks)) axes.push('remark')
|
|
148
146
|
return axes
|
|
149
147
|
}
|
package/spec-yatsu/src/proof.ts
CHANGED
|
@@ -19,7 +19,8 @@ export type ProofReading = {
|
|
|
19
19
|
fresh: boolean
|
|
20
20
|
staleAxes: string[]
|
|
21
21
|
score: ScoreState
|
|
22
|
-
|
|
22
|
+
// legacy instrument tag ('manual@1') — present on old readings only.
|
|
23
|
+
evaluator?: string
|
|
23
24
|
ts: string
|
|
24
25
|
evidence:
|
|
25
26
|
| { kind: 'image'; dataUri: string }
|
|
@@ -340,7 +341,7 @@ function renderReading(r: ProofReading): string {
|
|
|
340
341
|
<span class="scenario">${esc(r.scenario)}</span>
|
|
341
342
|
${verdictBadge(r.verdict)}
|
|
342
343
|
${stale}
|
|
343
|
-
<span class="rmeta">${esc(r.evaluator)} · ${esc(r.ts)}</span>
|
|
344
|
+
<span class="rmeta">${r.evaluator ? `${esc(r.evaluator)} · ` : ''}${esc(r.ts)}</span>
|
|
344
345
|
</div>
|
|
345
346
|
${r.expected ? `<div class="expected"><b>expected</b> ${esc(r.expected)}</div>` : ''}
|
|
346
347
|
${note}
|
|
@@ -172,13 +172,35 @@ export function scenarioChangeCommits(idx: ScenarioIndex, yatsuPath: string, sce
|
|
|
172
172
|
return idx.get(yatsuPath)?.get(scenario) ?? []
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// (root, sha, path) -> the blob oid at that commit ('' = unresolvable: path absent there, or the commit
|
|
176
|
+
// object gone). A FULL sha names an immutable tree, so entries never invalidate (freshness.ts's diffMemo
|
|
177
|
+
// reasoning); the LRU only bounds memory, sized above the largest adopter reading corpus — one entry per
|
|
178
|
+
// (reading, path) worst case — so a repeat board build never thrashes back into forking. This memo is what
|
|
179
|
+
// keeps a fully off-history corpus (an adopter history rewrite) cheap on REPEAT builds: without it every
|
|
180
|
+
// scenarioDiffers call re-forked `git rev-parse` for BOTH sides, per reading, per build (spexcode#39). A
|
|
181
|
+
// symbolic rev (HEAD, a branch) moves, so it resolves live and is never cached.
|
|
182
|
+
const FULL_SHA = /^[0-9a-f]{40}$/
|
|
183
|
+
const oidMemo = new Map<string, string>()
|
|
184
|
+
function oidAt(root: string, rev: string, path: string): string {
|
|
185
|
+
const resolve = () => { try { return git(['-C', root, 'rev-parse', `${rev}:${path}`]).trim() } catch { return '' } }
|
|
186
|
+
if (!FULL_SHA.test(rev)) return resolve()
|
|
187
|
+
const k = `${root}\x1f${rev}\x1f${path}`
|
|
188
|
+
const hit = oidMemo.get(k)
|
|
189
|
+
if (hit !== undefined) { oidMemo.delete(k); oidMemo.set(k, hit); return hit }
|
|
190
|
+
const v = resolve()
|
|
191
|
+
oidMemo.set(k, v)
|
|
192
|
+
if (oidMemo.size > 4096) oidMemo.delete(oidMemo.keys().next().value!)
|
|
193
|
+
return v
|
|
194
|
+
}
|
|
195
|
+
|
|
175
196
|
// canonical per-scenario SEMANTIC blocks of `rev:path` (the blockContent projection), for the off-history
|
|
176
|
-
// content fallback ([[yatsu-core]]'s ContentProbe): resolve the blob oid first
|
|
177
|
-
// so an unchanged file usually hits blockByOid straight from the index
|
|
178
|
-
// miss. null = the path is unreadable at that rev (absent, renamed
|
|
197
|
+
// content fallback ([[yatsu-core]]'s ContentProbe): resolve the blob oid first (memoized above for a full
|
|
198
|
+
// sha) — oids are content-addressed, so an unchanged file usually hits blockByOid straight from the index
|
|
199
|
+
// build — and parse only on a genuine miss. null = the path is unreadable at that rev (absent, renamed
|
|
200
|
+
// since, or the rev itself is gone).
|
|
179
201
|
export function scenarioBlocksAt(root: string, rev: string, path: string): Map<string, string> | null {
|
|
180
|
-
|
|
181
|
-
|
|
202
|
+
const oid = oidAt(root, rev, path)
|
|
203
|
+
if (!oid) return null
|
|
182
204
|
const hit = blockByOid.get(oid)
|
|
183
205
|
if (hit) return hit
|
|
184
206
|
try {
|
|
@@ -22,8 +22,8 @@ export type Evidence = { hash: string; kind: EvidenceKind }
|
|
|
22
22
|
// (timeline.ts) mapping clip moments to named steps — it anchors the reading's VIDEO evidence entry.
|
|
23
23
|
// `by` is the SESSION that filed this reading (the filer, from envSessionId) — the ORIGINATOR an eval-comment
|
|
24
24
|
// thread loops in on a reply ([[mentions]] implicit loop-in). Pure additive: a legacy reading without it simply
|
|
25
|
-
// has no originator → silent.
|
|
26
|
-
//
|
|
25
|
+
// has no originator → silent. WHO measured is deliberately NOT a schema axis — the agent is the measuring
|
|
26
|
+
// hand; the retired `evaluator` tag survives on old lines only, read-tolerated like the scalar blob.
|
|
27
27
|
export type Reading = {
|
|
28
28
|
scenario: string
|
|
29
29
|
codeSha: string
|
|
@@ -32,7 +32,8 @@ export type Reading = {
|
|
|
32
32
|
blob?: string | null
|
|
33
33
|
blobKind?: EvidenceKind
|
|
34
34
|
timelineBlob?: string
|
|
35
|
-
|
|
35
|
+
// legacy instrument tag (always 'manual@1') — read for old readings, never written by new filings.
|
|
36
|
+
evaluator?: string
|
|
36
37
|
by?: string
|
|
37
38
|
verdict?: Verdict
|
|
38
39
|
ts: string
|
|
@@ -65,10 +66,9 @@ export function isJsonBlob(b: Buffer): boolean {
|
|
|
65
66
|
|
|
66
67
|
// a RETRACTION is the sanctioned inverse of a filing — itself an appended event, never a deleted line
|
|
67
68
|
// (the sidecar stays append-only; git shows who retracted what, when). `retracts` is the target reading's
|
|
68
|
-
// `ts` within `scenario` (its natural key).
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// says why (a botched e2e filing, a wrong verdict).
|
|
69
|
+
// `ts` within `scenario` (its natural key). The two event kinds are told apart POSITIVELY — a retraction
|
|
70
|
+
// carries `retracts`, a reading carries `codeSha`; neither is recognized by another field's absence. `by`
|
|
71
|
+
// is the retracting session; `note` says why (a botched e2e filing, a wrong verdict).
|
|
72
72
|
export type Retraction = { retracts: string; scenario: string; note?: string; by?: string; ts: string }
|
|
73
73
|
|
|
74
74
|
// parse the sidecar RAW: one event per non-blank line — a Reading, or a Retraction (a line carrying a
|
|
@@ -85,7 +85,7 @@ export function readSidecar(sidecarPath: string): { readings: Reading[]; retract
|
|
|
85
85
|
const r = JSON.parse(t)
|
|
86
86
|
if (!r || typeof r.scenario !== 'string') continue
|
|
87
87
|
if (typeof r.retracts === 'string') retractions.push(r as Retraction)
|
|
88
|
-
else if (typeof r.
|
|
88
|
+
else if (typeof r.codeSha === 'string') readings.push(r as Reading)
|
|
89
89
|
} catch { /* skip a malformed line */ }
|
|
90
90
|
}
|
|
91
91
|
return { readings, retractions }
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export const EVALUATORS: Record<string, number> = { manual: 1 }
|
|
2
|
-
export const DEFAULT_EVALUATOR = 'manual'
|
|
3
|
-
|
|
4
|
-
// the tag stamped on a reading. With no name → the default `manual`; an unknown name still tags (version 1)
|
|
5
|
-
// so an out-of-band evaluator can record without the core having to know it yet.
|
|
6
|
-
export function evaluatorTag(name: string = DEFAULT_EVALUATOR): string {
|
|
7
|
-
return `${name}@${EVALUATORS[name] ?? 1}`
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// parse a recorded evaluator tag back into name + version (for comparing against the current version).
|
|
11
|
-
export function parseEvaluator(tag: string): { name: string; version: number } {
|
|
12
|
-
const at = tag.lastIndexOf('@')
|
|
13
|
-
if (at < 0) return { name: tag, version: NaN }
|
|
14
|
-
return { name: tag.slice(0, at), version: Number(tag.slice(at + 1)) }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// a reading's evaluator tag is stale iff its evaluator is KNOWN to the core and its version is behind the
|
|
18
|
-
// current one. An unknown evaluator invents no staleness — we can't version an instrument we don't define.
|
|
19
|
-
export function isEvaluatorStale(tag: string): boolean {
|
|
20
|
-
const { name, version } = parseEvaluator(tag)
|
|
21
|
-
const cur = EVALUATORS[name]
|
|
22
|
-
if (cur === undefined) return false
|
|
23
|
-
return version !== cur
|
|
24
|
-
}
|