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.
Files changed (26) hide show
  1. package/README.md +4 -2
  2. package/package.json +1 -1
  3. package/spec-cli/src/cli.ts +9 -1
  4. package/spec-cli/src/guide.ts +23 -16
  5. package/spec-cli/src/harness.ts +18 -9
  6. package/spec-cli/src/help.ts +4 -1
  7. package/spec-cli/src/sessions.ts +34 -15
  8. package/spec-cli/templates/spec/project/.config/supervisor/spec.md +1 -1
  9. package/spec-dashboard/dist/assets/{Dashboard-BlRRsxE7.js → Dashboard-P0B9ukSG.js} +3 -3
  10. package/spec-dashboard/dist/assets/{EvalsPage-BzVE38-Z.js → EvalsPage-BrvAGyc4.js} +1 -1
  11. package/spec-dashboard/dist/assets/{FoldToggle-DFuLVOeu.js → FoldToggle-BuQ0lokE.js} +1 -1
  12. package/spec-dashboard/dist/assets/{IssuesPage-CzDaazhe.js → IssuesPage-H-D8aHEl.js} +1 -1
  13. package/spec-dashboard/dist/assets/{MobileApp-CXQrQCNp.js → MobileApp-oZXIeCPb.js} +1 -1
  14. package/spec-dashboard/dist/assets/{SessionInterface-D1pUBl6q.js → SessionInterface-Blr_MEdU.js} +1 -1
  15. package/spec-dashboard/dist/assets/{SessionWindow-Y25Bwg1e.js → SessionWindow-LcCzBMU7.js} +2 -2
  16. package/spec-dashboard/dist/assets/{Settings-R610Vbzd.js → Settings-_yOye-In.js} +1 -1
  17. package/spec-dashboard/dist/assets/{index-BO0Zuweu.js → index-BhIslAau.js} +2 -2
  18. package/spec-dashboard/dist/index.html +1 -1
  19. package/spec-yatsu/src/cli.ts +3 -5
  20. package/spec-yatsu/src/evaltab.ts +3 -2
  21. package/spec-yatsu/src/filing.ts +3 -4
  22. package/spec-yatsu/src/freshness.ts +1 -3
  23. package/spec-yatsu/src/proof.ts +3 -2
  24. package/spec-yatsu/src/scenariofresh.ts +27 -5
  25. package/spec-yatsu/src/sidecar.ts +8 -8
  26. 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-BO0Zuweu.js"></script>
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>
@@ -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)} [${reading.evaluator}] (${ev})`)
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)} [${target.evaluator}]) retracted — ${now}`)
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.evaluator} ${r.codeSha.slice(0, 7)} ${ev} ${r.ts}`
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
- evaluator: string
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,
@@ -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; the
11
- // evaluator is the human hand, manual@1. yatsu still runs nothing — this only records. The dashboard files
12
- // nothing through this: [[event-detail]] is read-side on readings.
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' | 'evaluator' | 'remark' | 'anchor'
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
  }
@@ -19,7 +19,8 @@ export type ProofReading = {
19
19
  fresh: boolean
20
20
  staleAxes: string[]
21
21
  score: ScoreState
22
- evaluator: string
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 oids are content-addressed,
177
- // so an unchanged file usually hits blockByOid straight from the index build — and parse only on a genuine
178
- // miss. null = the path is unreadable at that rev (absent, renamed since, or the rev itself is gone).
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
- let oid: string
181
- try { oid = git(['-C', root, 'rev-parse', `${rev}:${path}`]).trim() } catch { return null }
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. `evaluator` is WHO/WHAT measured (a tag like `manual@1`); `by` is the reachable
26
- // session behind the filing two different axes.
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
- evaluator: string
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). Deliberately NO `evaluator` field: an old reader's line filter
69
- // (which requires an evaluator string) skips a retraction entirely, so version skew degrades to "the
70
- // retraction isn't applied yet", never to a mis-rendered reading. `by` is the retracting session; `note`
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.evaluator === 'string') readings.push(r as Reading)
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
- }