@spexcode/spec-eval 0.6.7 → 0.7.0-next.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/dist/cli.js CHANGED
@@ -203,7 +203,7 @@ async function scan(args = []) {
203
203
  }
204
204
  // an anchor is a claim a named unit exists — dead/ambiguous/unextractable is LOUD, and until it is
205
205
  // repaired the probe issues no verdict, so the reading stays conservatively stale.
206
- for (const p of [...axis.problems, ...anchorProblems(root, axis.entries)]) {
206
+ for (const p of [...axis.problems, ...(await anchorProblems(root, axis.entries))]) {
207
207
  malformed++;
208
208
  findings.push(` • eval-schema: '${s.id}' scenario '${sc.name}' ${p} — fix ${y.evalPath}`);
209
209
  }
package/dist/evaltab.d.ts CHANGED
@@ -83,6 +83,7 @@ export type EvalContext = {
83
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
84
  export declare function evalTimelines(ids: readonly string[], ctx?: EvalContext, opts?: {
85
85
  order?: boolean;
86
+ latestOnly?: boolean;
86
87
  }): Promise<EvalTimeline[]>;
87
88
  export declare function evalTimeline(id: string, ctx?: EvalContext): Promise<EvalTimeline>;
88
89
  export type BlobResult = {
package/dist/evaltab.js CHANGED
@@ -50,9 +50,14 @@ export async function evalTimelines(ids, ctx, opts = {}) {
50
50
  const codeEntries = specs.find((s) => dirname(s.path) === relative(root, ynode.dir))?.codeEntries ?? [];
51
51
  const byName = new Map(ynode.scenarios.map((s) => [s.name, s]));
52
52
  const { readings, retractions, oks } = readSidecar(ynode.sidecarPath);
53
- const rows = applyRetractions(readings, retractions).map((reading) => ({
53
+ const allRows = applyRetractions(readings, retractions).map((reading) => ({
54
54
  reading, axis: scenarioCodeAxis(byName.get(reading.scenario)?.code, codeEntries),
55
55
  }));
56
+ // The board only renders the latest state per scenario. Historical rows remain available to the
57
+ // session/eval detail paths, but sending them through freshness here multiplies immutable Git probes.
58
+ const rows = opts.latestOnly
59
+ ? [...allRows.reduce((latest, row) => latest.set(row.reading.scenario, row), new Map()).values()]
60
+ : allRows;
56
61
  return { id, ynode, codeEntries, rows, retractions, oks };
57
62
  });
58
63
  // an order-only read stops HERE, before a single probe: the rows above are already the whole answer.
@@ -30,7 +30,7 @@ export type AnchorDemand = {
30
30
  entries: readonly RelationEntry[];
31
31
  };
32
32
  export declare function anchorProbeFor(root: string, idx: DriftIndex): AnchorProbe;
33
- export declare function anchorProblems(root: string, entries: readonly RelationEntry[]): string[];
33
+ export declare function anchorProblems(root: string, entries: readonly RelationEntry[]): Promise<string[]>;
34
34
  export type RemarkSignal = {
35
35
  resolved: boolean;
36
36
  resolvedAt?: string;
package/dist/freshness.js CHANGED
@@ -497,7 +497,7 @@ const currentTreeUnitMemo = new Map();
497
497
  // caller retaining a verdict derived from these units names it by the same identity that decides the parse
498
498
  // ([[selector-anchor-scope]]). Returning the key rather than re-deriving it elsewhere is what keeps the two
499
499
  // from drifting apart.
500
- function currentTreeImage(root, x, path) {
500
+ async function currentTreeImage(root, x, path) {
501
501
  const source = readFileSync(join(root, path), 'utf8');
502
502
  const key = `${x.memoKey(path)}\0${createHash('sha1').update(source).digest('hex')}`;
503
503
  const hit = currentTreeUnitMemo.get(key);
@@ -508,7 +508,7 @@ function currentTreeImage(root, x, path) {
508
508
  }
509
509
  let entry;
510
510
  try {
511
- entry = { units: x.extract(source, path) };
511
+ entry = { units: await x.extract(source, path) };
512
512
  }
513
513
  catch (err) {
514
514
  entry = { failed: err?.message ?? String(err) };
@@ -520,15 +520,15 @@ function currentTreeImage(root, x, path) {
520
520
  throw new Error(entry.failed);
521
521
  return { key, units: entry.units };
522
522
  }
523
- function entryImage(root, regs, path, selector) {
523
+ async function entryImage(root, regs, path, selector) {
524
524
  const x = extractorFor(regs, extOf(path));
525
525
  if (!x)
526
526
  return { problem: `\`code\` selector \`${path}#${selector}\` — no designated extractor for that language; drop the #anchor or add a language row` };
527
- const ready = x.ready();
527
+ const ready = await x.ready();
528
528
  if (ready !== true)
529
529
  return { problem: `\`code\` anchors on ${path} are unverified: ${ready}` };
530
530
  try {
531
- return currentTreeImage(root, x, path);
531
+ return await currentTreeImage(root, x, path);
532
532
  }
533
533
  catch (err) {
534
534
  return { problem: `\`code\` anchors on ${path} are unverified: ${err?.message ?? String(err)}` };
@@ -550,8 +550,8 @@ function selectorProblem(units, path, selectors) {
550
550
  }
551
551
  return null;
552
552
  }
553
- function entryUnverifiable(root, regs, entry) {
554
- const image = entryImage(root, regs, entry.path, entry.selectors[0]);
553
+ async function entryUnverifiable(root, regs, entry) {
554
+ const image = await entryImage(root, regs, entry.path, entry.selectors[0]);
555
555
  return 'problem' in image ? image.problem : selectorProblem(image.units, entry.path, entry.selectors);
556
556
  }
557
557
  // @@@ the verify sweep must reach the MACROTASK queue - resolving every reading's `code:` selectors against
@@ -603,10 +603,10 @@ export function anchorProbeFor(root, idx) {
603
603
  // one coherent read of each source per sweep: every entry on a path resolves against the same bytes,
604
604
  // and the read that names the verdict is the read the verdict was derived from.
605
605
  const images = new Map();
606
- const imageOf = (path, selector) => {
606
+ const imageOf = async (path, selector) => {
607
607
  let hit = images.get(path);
608
608
  if (hit === undefined) {
609
- hit = entryImage(root, regs, path, selector);
609
+ hit = await entryImage(root, regs, path, selector);
610
610
  images.set(path, hit);
611
611
  }
612
612
  return hit;
@@ -630,7 +630,7 @@ export function anchorProbeFor(root, idx) {
630
630
  const key = anchorKey(sinceSha, e.path, e.selectors);
631
631
  if (verdicts.has(key) || queued.has(key))
632
632
  continue;
633
- const image = imageOf(e.path, e.selectors[0]);
633
+ const image = await imageOf(e.path, e.selectors[0]);
634
634
  const name = 'problem' in image ? null : `${registry}\x1f${image.key}\x1f${key}`;
635
635
  const remembered = scope && name !== null ? scope.anchorVerdicts.get(name) : undefined;
636
636
  if (remembered !== undefined) {
@@ -666,13 +666,13 @@ export function anchorProbeFor(root, idx) {
666
666
  // the LOUD half: a selector is a claim that a named unit EXISTS, held to the same standard as a ghost path.
667
667
  // Dead, ambiguous, unparseable, or no designated extractor — each names itself and its repair, and until
668
668
  // repaired the probe issues no verdict, so the reading stays stale. Over-warn, never a silent pass.
669
- export function anchorProblems(root, entries) {
669
+ export async function anchorProblems(root, entries) {
670
670
  const regs = extractors(root);
671
671
  const out = [];
672
672
  for (const e of entries) {
673
673
  if (!e.selectors.length)
674
674
  continue;
675
- const problem = entryUnverifiable(root, regs, e);
675
+ const problem = await entryUnverifiable(root, regs, e);
676
676
  if (problem)
677
677
  out.push(problem);
678
678
  }
package/dist/host.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { RemarkTrack } from './remarks.js';
2
+ import type { ReviewDiffFile } from '@spexcode/spec-core';
2
3
  export type EvalHost = {
3
4
  loadConfig?: (root: string) => any;
4
5
  trackedSourceFiles?: (root: string, roots: string[], policy: any) => string[];
@@ -19,7 +20,7 @@ export type ReviewPayload = {
19
20
  label: string;
20
21
  ahead: number;
21
22
  dirtyNonRuntime: number;
22
- diff: import('@spexcode/spec-core').ReviewDiffFile[];
23
+ diff: ReviewDiffFile[];
23
24
  gates: {
24
25
  conflictsWithMain: boolean;
25
26
  lint: {
@@ -218,7 +218,7 @@ export type SessionEvalRevision = {
218
218
  export type SessionEvalProjection = {
219
219
  epoch: string;
220
220
  generation: number;
221
- phase: 'loading' | 'updating' | 'ready' | 'error';
221
+ phase: 'loading' | 'updating' | 'ready' | 'error' | 'dormant';
222
222
  revision?: string;
223
223
  value?: SessionEvalSummary;
224
224
  lastKnown?: {
@@ -279,6 +279,7 @@ export declare class SessionEvalProjectionCache {
279
279
  demand<T>(id: string, path: string, run: () => Promise<T>): Promise<T>;
280
280
  idle(): Promise<void>;
281
281
  accept(id: string, generation: number, revision: string, value: SessionEvalSummary): boolean;
282
+ private dormantEntry;
282
283
  private project;
283
284
  private matches;
284
285
  private publish;
@@ -116,7 +116,7 @@ async function validateSelectorEntry(context, revision, entry, sourceView) {
116
116
  if (!x) {
117
117
  throw new SessionImpactUnavailableError(`session impact selector '${entry.path}#${entry.selectors[0]}' is unavailable: no designated extractor for '.${extOf(entry.path)}'`);
118
118
  }
119
- const ready = x.ready();
119
+ const ready = await x.ready();
120
120
  if (ready !== true) {
121
121
  throw new SessionImpactUnavailableError(`session impact selectors on '${entry.path}' are unavailable: ${ready}`);
122
122
  }
@@ -132,7 +132,7 @@ async function validateSelectorEntry(context, revision, entry, sourceView) {
132
132
  throw new SessionImpactUnavailableError(`session impact selector '${entry.path}#${entry.selectors[0]}' is dead at ${revision.slice(0, 8)}: the file does not exist`);
133
133
  }
134
134
  try {
135
- return { extractor: x, units: x.extract(source, entry.path) };
135
+ return { extractor: x, units: await x.extract(source, entry.path) };
136
136
  }
137
137
  catch (error) {
138
138
  throw new SessionImpactUnavailableError(`session impact selectors on '${entry.path}' are unextractable at ${revision.slice(0, 8)}: ${error?.message ?? String(error)}`);
@@ -1636,6 +1636,16 @@ export class SessionEvalProjectionCache {
1636
1636
  promise.finally(() => {
1637
1637
  if (this.demands.get(id) === promise)
1638
1638
  this.demands.delete(id);
1639
+ // The cancel above exists ONLY so the warmup does not duplicate the build this demand is about to run.
1640
+ // Once the demand has settled that reason is spent — and a demand that REJECTED published nothing, so
1641
+ // leaving the mark set kept `authorize` refusing this generation forever and the entry stuck at
1642
+ // `loading`/`updating` with no build ever scheduled. A demand that published is already `ready` and
1643
+ // needs no second build.
1644
+ if (this.entries.get(id) !== entry || entry.demandCancelledGeneration !== entry.generation)
1645
+ return;
1646
+ entry.demandCancelledGeneration = null;
1647
+ this.authorize(entry);
1648
+ queueMicrotask(() => this.startScheduled());
1639
1649
  }).catch(() => { });
1640
1650
  return promise;
1641
1651
  }
@@ -1660,12 +1670,19 @@ export class SessionEvalProjectionCache {
1660
1670
  this.notify();
1661
1671
  return true;
1662
1672
  }
1673
+ // Dormant offline history is demand-only BY POLICY: `authorize` refuses to schedule an offline entry, so
1674
+ // nothing will move it until a demand builds it. `loading`/`updating` are arrival phases — a reader that
1675
+ // renders them as progress waits forever. This names the difference at the one place that knows it.
1676
+ dormantEntry(entry) {
1677
+ return entry.liveness === 'offline' && entry.running == null && entry.scheduled == null
1678
+ && entry.observerHolds.size === 0 && entry.phase !== 'ready' && entry.phase !== 'error';
1679
+ }
1663
1680
  project(entry) {
1664
1681
  const stable = entry.current;
1665
1682
  return {
1666
1683
  epoch: this.epoch,
1667
1684
  generation: entry.generation,
1668
- phase: entry.phase,
1685
+ phase: this.dormantEntry(entry) ? 'dormant' : entry.phase,
1669
1686
  ...(entry.phase === 'ready' && stable
1670
1687
  ? { revision: stable.revision, value: stable.value }
1671
1688
  : stable ? { lastKnown: stable } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-eval",
3
- "version": "0.6.7",
3
+ "version": "0.7.0-next.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
27
27
  },
28
28
  "dependencies": {
29
- "@spexcode/spec-core": "0.6.7"
29
+ "@spexcode/spec-core": "0.7.0-next.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^20.16.0",