spexcode 0.6.0 → 0.6.2

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 (88) hide show
  1. package/README.md +97 -149
  2. package/package.json +1 -1
  3. package/spec-cli/hooks/harness.sh +26 -6
  4. package/spec-cli/src/anchors.ts +27 -5
  5. package/spec-cli/src/cli.ts +239 -75
  6. package/spec-cli/src/client.ts +113 -14
  7. package/spec-cli/src/codex-runtime-generations.ts +97 -7
  8. package/spec-cli/src/commit-surgery.ts +2 -1
  9. package/spec-cli/src/contract-filter.ts +58 -42
  10. package/spec-cli/src/delivery-queue.ts +62 -4
  11. package/spec-cli/src/doctor.ts +2 -1
  12. package/spec-cli/src/execution-trace.ts +444 -0
  13. package/spec-cli/src/file-write.ts +22 -0
  14. package/spec-cli/src/gateway-hub.ts +10 -8
  15. package/spec-cli/src/gateway.ts +99 -6
  16. package/spec-cli/src/git.ts +503 -105
  17. package/spec-cli/src/graph.ts +56 -6
  18. package/spec-cli/src/graphCache.ts +86 -17
  19. package/spec-cli/src/graphStream.ts +79 -11
  20. package/spec-cli/src/guide.ts +93 -4
  21. package/spec-cli/src/harness-select.ts +2 -2
  22. package/spec-cli/src/harness.ts +355 -75
  23. package/spec-cli/src/help.ts +30 -23
  24. package/spec-cli/src/host-resources.ts +62 -7
  25. package/spec-cli/src/identity-presets.js +16 -6
  26. package/spec-cli/src/index.ts +109 -14
  27. package/spec-cli/src/init.ts +8 -17
  28. package/spec-cli/src/layout.ts +151 -40
  29. package/spec-cli/src/lint.ts +37 -7
  30. package/spec-cli/src/materialize.ts +160 -112
  31. package/spec-cli/src/plugin-harness.ts +29 -18
  32. package/spec-cli/src/process-identity.ts +13 -0
  33. package/spec-cli/src/reviews.ts +103 -30
  34. package/spec-cli/src/session-execution.ts +68 -0
  35. package/spec-cli/src/session-files.ts +121 -0
  36. package/spec-cli/src/session-follow.ts +4 -4
  37. package/spec-cli/src/session-reparent.ts +33 -0
  38. package/spec-cli/src/session-timeline.ts +163 -14
  39. package/spec-cli/src/session-web.ts +135 -0
  40. package/spec-cli/src/sessions.ts +748 -111
  41. package/spec-cli/src/uninstall.ts +3 -2
  42. package/spec-cli/templates/hooks/post-checkout +3 -0
  43. package/spec-cli/templates/hooks/post-merge +1 -0
  44. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +1 -1
  45. package/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +2 -21
  46. package/spec-cli/templates/spec/project/.plugins/core/spec.md +12 -13
  47. package/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
  48. package/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +2 -2
  49. package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +5 -22
  50. package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +2 -1
  51. package/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +4 -6
  52. package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +3 -9
  53. package/spec-dashboard/dist/assets/{App-b8Nh0sgk.js → App-B6IbFbGu.js} +2 -2
  54. package/spec-dashboard/dist/assets/Dashboard-_sDk7Rcg.js +27 -0
  55. package/spec-dashboard/dist/assets/EvalsPage-BT357Lyt.js +2 -0
  56. package/spec-dashboard/dist/assets/{IssuesPage-CAP64YWE.js → IssuesPage-BG9rNOgV.js} +1 -1
  57. package/spec-dashboard/dist/assets/MobileApp-D9yPMR3A.js +2 -0
  58. package/spec-dashboard/dist/assets/{Modal-Drscez-d.js → Modal-B3EcgeA3.js} +1 -1
  59. package/spec-dashboard/dist/assets/{PageScroll-qW6uOJL8.js → PageScroll-CNZOugWV.js} +1 -1
  60. package/spec-dashboard/dist/assets/{ProjectsPage-CjybFBmR.js → ProjectsPage-C2CRHmvs.js} +1 -1
  61. package/spec-dashboard/dist/assets/SessionInterface-DOEjuT3l.js +40 -0
  62. package/spec-dashboard/dist/assets/{SessionWindow-iOk0yHoU.js → SessionWindow-1nYgZ4CL.js} +1 -1
  63. package/spec-dashboard/dist/assets/Settings-7WeTJn0V.js +1 -0
  64. package/spec-dashboard/dist/assets/Thread-CEbzAFQn.js +13 -0
  65. package/spec-dashboard/dist/assets/data-SlvxNA2r.js +1 -0
  66. package/spec-dashboard/dist/assets/{index-paP-z_Vd.js → index-DLVeOHL-.js} +10 -10
  67. package/spec-dashboard/dist/assets/index-DYLGfPjx.css +1 -0
  68. package/spec-dashboard/dist/assets/{launch-B-bYdWmh.js → launch-yxCYr64x.js} +29 -29
  69. package/spec-dashboard/dist/assets/sessionSurface--7l69h4F.js +1 -0
  70. package/spec-dashboard/dist/index.html +2 -2
  71. package/spec-dashboard/src/reviewFilters.js +5 -0
  72. package/spec-dashboard/src/session.js +5 -1
  73. package/spec-eval/src/cli.ts +13 -9
  74. package/spec-eval/src/evaltab.ts +32 -10
  75. package/spec-eval/src/freshness.ts +404 -81
  76. package/spec-eval/src/scenariofresh.ts +58 -8
  77. package/spec-eval/src/scenarios.ts +60 -12
  78. package/spec-eval/src/sessioneval.ts +234 -148
  79. package/spec-dashboard/dist/assets/Dashboard-CvAjfRC2.js +0 -27
  80. package/spec-dashboard/dist/assets/EvalsPage-Bz-nMKoS.js +0 -2
  81. package/spec-dashboard/dist/assets/MobileApp-D9L1Va8Z.js +0 -2
  82. package/spec-dashboard/dist/assets/SessionInterface-Dl9v0JFM.js +0 -39
  83. package/spec-dashboard/dist/assets/Settings-BZ1lGRJs.js +0 -1
  84. package/spec-dashboard/dist/assets/Thread-D_kcDnfd.js +0 -13
  85. package/spec-dashboard/dist/assets/data-Bwd3kAVL.js +0 -1
  86. package/spec-dashboard/dist/assets/index-DAbQBBK_.css +0 -1
  87. package/spec-dashboard/dist/assets/terminalFont-C6oGNKrf.js +0 -1
  88. package/spec-eval/src/matrix.ts +0 -693
@@ -0,0 +1 @@
1
+ import{P as N}from"./index-DLVeOHL-.js";const u="spexcode.terminal-font-size",T=9,g=18,c=1,r=new Set;function b(){const e=Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--type-terminal"));if(!Number.isFinite(e))throw new Error("Terminal font-size token is missing or invalid");return e}function f(e){const t=Number(e);return Number.isFinite(t)&&t>=T&&t<=g?Math.round(t/c)*c:null}function _(){let e=null;try{e=f(localStorage.getItem(u))}catch{}return e??b()}function p(e){const t=f(e);if(t===null)throw new Error(`Invalid terminal font size: ${e}`);try{localStorage.setItem(u,String(t))}catch{}for(const s of r)s(t);return t}function w(e){return r.add(e),()=>r.delete(e)}const S="terminal",y="conversation",F="spexcode.session-surface.v1",A=new Set([S,y]),o=new Set;let l=null;const d=(e=N)=>`${F}.${e||"root"}`,i=()=>({defaultSurface:S,sessions:{}}),a=e=>A.has(e);function I(e){const t=i();return!e||typeof e!="object"||(a(e.defaultSurface)&&(t.defaultSurface=e.defaultSurface),e.sessions&&typeof e.sessions=="object"&&!Array.isArray(e.sessions)&&(t.sessions=Object.fromEntries(Object.entries(e.sessions).filter(([,s])=>a(s))))),t}function n(){try{const e=localStorage.getItem(d());return e?I(JSON.parse(e)):i()}catch{return l||i()}}function m(e){l=e;try{localStorage.setItem(d(),JSON.stringify(e))}catch{}for(const t of o)t(e);return e}function E(e){if(!a(e))throw new Error(`Invalid session surface: ${e}`);return e}function R(){return n().defaultSurface}function h(e){const t=n();return t.sessions[e]||t.defaultSurface}function z(e){const t=n();return m({...t,defaultSurface:E(e)}).defaultSurface}function M(e,t){if(!e)throw new Error("A session id is required to set its base surface");const s=n();return m({...s,sessions:{...s.sessions,[e]:E(t)}}).sessions[e]}function C(e){return o.add(e),()=>o.delete(e)}export{S,c as T,R as a,y as b,g as c,T as d,z as e,w as f,_ as g,C as h,h as i,M as j,p as s};
@@ -20,8 +20,8 @@
20
20
  } catch (e) { /* localStorage unavailable — the bare :root default (Minimal) stands */ }
21
21
  })()
22
22
  </script>
23
- <script type="module" crossorigin src="/assets/index-paP-z_Vd.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-DAbQBBK_.css">
23
+ <script type="module" crossorigin src="/assets/index-DLVeOHL-.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-DYLGfPjx.css">
25
25
  </head>
26
26
  <body>
27
27
  <div id="root"></div>
@@ -169,6 +169,11 @@ const reviewStateOf = (entry) => (evalIsResult(entry) && entry.fresh && entry.hu
169
169
  const evalFresh = (entry) => entry.fresh === true
170
170
  const freshnessOf = (entry) => (evalIsResult(entry) ? (evalFresh(entry) ? 'fresh' : 'stale') : null)
171
171
  export const evalReviewState = (reading) => {
172
+ // an order-only row never had its freshness computed, so it has no verdict to report. Refuse it here
173
+ // rather than let its conservative placeholder be published as a measured `stalePass`/`staleFail`: the
174
+ // whole point of the deferred pass is that these rows exist only to establish sequence.
175
+ if (reading?.freshnessDeferred)
176
+ throw new Error(`eval row ${reading.scenario ?? '?'} has deferred freshness — it can order a list, never state a verdict`)
172
177
  const status = reading?.verdict?.status
173
178
  if (status !== 'pass' && status !== 'fail') return 'empty'
174
179
  if (reading.fresh) return status
@@ -88,7 +88,11 @@ export const sessionHandle = (s) =>
88
88
  s?.label || s?.name || s?.node || s?.title || s?.branch || s?.id
89
89
 
90
90
  export const sessionHeadline = (s) =>
91
- s?.headline || s?.name || s?.activity || s?.promptPreview || s?.node || s?.title || s?.branch || s?.id
91
+ s?.title || s?.headline || s?.name || s?.activity || s?.note || s?.promptPreview || s?.node || s?.raw?.title || s?.branch || s?.id
92
+
93
+ // The visible session name is a single derived `title` on current backends. Keep the old
94
+ // accessor as a compatibility alias while mixed-version clients roll forward.
95
+ export const sessionTitle = sessionHeadline
92
96
 
93
97
  export function nestSessions(sessions) {
94
98
  const present = new Set(sessions.map((s) => s?.id))
@@ -7,7 +7,7 @@ import { trackedSourceFiles } from '../../spec-cli/src/source-files.js'
7
7
  import { mainBranch, envSessionId, readRawRecord } from '../../spec-cli/src/layout.js'
8
8
  import { evalNodes, evalNodesAt, validateScenarios, resolveEvalNode, scenarioCodeAxis, scenarioHash, scenarioProjection, writeScenarioMeasurementMetadata, EVAL_FILE, type EvalNode, type ScenarioTestReference } from './scenarios.js'
9
9
  import { readReadings, readSidecar, appendReading, appendRetraction, latestPerScenario, evidenceOf, isJsonBlob, type Reading, type Verdict, type Evidence, type EvidenceKind, type Retraction } from './sidecar.js'
10
- import { staleAxes, contentProbeFor, anchorProbeFor, anchorProblems, type AnchorDemand } from './freshness.js'
10
+ import { staleAxes, contentProbeFor, anchorProbeFor, anchorProblems, type AnchorDemand, type ContentProbeDemand } from './freshness.js'
11
11
  import { parseRelation, relationClaimsPath } from '../../spec-cli/src/anchors.js'
12
12
  import { scenarioIndex } from './scenariofresh.js'
13
13
  import { loadEvalRemarkTracks, trackKey } from '../../spec-cli/src/issues.js'
@@ -144,6 +144,7 @@ async function scan(args: string[] = []): Promise<number> {
144
144
  // the in-loop primes stay as the correctness backstop and become cache hits.
145
145
  const latestByDir = new Map<string, ReturnType<typeof latestPerScenario>>()
146
146
  const anchorDemands: AnchorDemand[] = []
147
+ const contentDemands: ContentProbeDemand[] = []
147
148
  for (const s of specs) {
148
149
  const dirRel = dirname(s.path)
149
150
  const y = yByDir.get(dirRel)
@@ -156,8 +157,12 @@ async function scan(args: string[] = []): Promise<number> {
156
157
  const axis = scenarioCodeAxis(sc.code, s.codeEntries)
157
158
  if (changed && !nodeChanged(dirRel, axis.paths, changed, nodeDirs)) continue
158
159
  anchorDemands.push({ sinceSha: r.codeSha, entries: axis.entries })
160
+ if (!commitReachable(idx, r.codeSha))
161
+ contentDemands.push({ anchorSha: r.codeSha, paths: axis.paths, evalPath: y.evalPath })
159
162
  }
160
163
  }
164
+ await probe.primeMany?.(contentDemands)
165
+ await probe.primeBlocks?.()
161
166
  await anchors.prime?.(anchorDemands)
162
167
  for (const s of specs) {
163
168
  const dirRel = dirname(s.path)
@@ -213,7 +218,12 @@ async function scan(args: string[] = []): Promise<number> {
213
218
  }
214
219
  if (!driftSelected) continue
215
220
  const remSignals = (remarkTracks.get(trackKey(s.id, sc.name))?.remarks ?? []).map((rm) => ({ resolved: !!rm.resolved, resolvedAt: rm.resolvedAt }))
216
- if (!commitReachable(idx, r.codeSha)) await probe.prime?.(r.codeSha, codeFiles, y.evalPath)
221
+ if (!commitReachable(idx, r.codeSha)) {
222
+ await probe.prime?.(r.codeSha, codeFiles, y.evalPath)
223
+ // lint decides this reading before moving on, so its demand is flushed here rather than pooled;
224
+ // the batched pair still beats the per-revision lookups it replaces.
225
+ await probe.primeBlocks?.()
226
+ }
217
227
  await anchors.prime?.([{ sinceSha: r.codeSha, entries: axis.entries }])
218
228
  const axes = staleAxes(r, axis.entries, y.evalPath, idx, scidx, remSignals, probe, sc, anchors)
219
229
  if (axes.length) {
@@ -803,19 +813,13 @@ export async function runEval(args: string[]): Promise<number> {
803
813
  if (sub === 'retract') return retractCmd(args.slice(1))
804
814
  if (sub === 'clean') return clean(args.slice(1))
805
815
  if (sub === 'ls') return show(args.slice(1))
806
- if (sub === 'matrix') {
807
- // the live-behavior matrix runner ([[live-matrix]]) — lazily imported so the heavy session machinery
808
- // never loads for the plain filing/reading verbs.
809
- const { runMatrix } = await import('./matrix.js')
810
- return runMatrix(args.slice(1))
811
- }
812
816
  if (sub === 'scenario') {
813
817
  if (args[1] === 'ls') return scenarioLs(args.slice(2))
814
818
  if (args[1] === 'write') return scenarioWrite(args.slice(2))
815
819
  console.error('spex eval scenario: ls [<node>|.] [--unmeasured] [--json] | write --mutation <json> < eval.md')
816
820
  return 2
817
821
  }
818
- console.error('spex eval: add [.|<node>] [--scenario <name>] (--pass|--fail) [--note <text>] [--image <path> …repeatable] [--result <path|->] [--video <path>] [--timeline <json>] | ls [.|<node>] [--json] | ls --session <SEL> [--export] | scenario ls [<node>|.] [--unmeasured] [--json] | scenario write --mutation <json> < eval.md | matrix <launcher> [--node <id>] [--rows k1,k2] | lint [--changed] | ok <node> [--scenario <name>] | retract [.|<node>] [--scenario <name>] [--last | --ts <iso>] [--note <why>] | clean [--keep-latest|--all]')
822
+ console.error('spex eval: add [.|<node>] [--scenario <name>] (--pass|--fail) [--note <text>] [--image <path> …repeatable] [--result <path|->] [--video <path>] [--timeline <json>] | ls [.|<node>] [--json] | ls --session <SEL> [--export] | scenario ls [<node>|.] [--unmeasured] [--json] | scenario write --mutation <json> < eval.md | lint [--changed] | ok <node> [--scenario <name>] | retract [.|<node>] [--scenario <name>] [--last | --ts <iso>] [--note <why>] | clean [--keep-latest|--all]')
819
823
  return 2
820
824
  }
821
825
 
@@ -38,6 +38,10 @@ export type EvalEntry = {
38
38
  ts: string
39
39
  fresh: boolean
40
40
  staleAxes: StaleAxis[]
41
+ // set by an order-only read, whose `fresh`/`staleAxes` are placeholders it never computed. A consumer
42
+ // that ignores the flag reads the CONSERVATIVE direction (stale), and evalReviewState refuses the row
43
+ // outright — so a deferred verdict can neither be published as fresh nor pass silently.
44
+ freshnessDeferred?: true
41
45
  blobState: 'present' | 'miss' | 'none'
42
46
  codeDrift?: { file: string; behind: number }[]
43
47
  remarks?: RemarkView[]
@@ -98,7 +102,12 @@ export async function evalContext(
98
102
  // per read rather than once per reading. evalTimelines is the plural the graph build actually wants: it
99
103
  // plans every node's rows first (pure fs + in-memory projection), primes the content and anchor probes with
100
104
  // the WHOLE demand set, then assembles. Singular evalTimeline is the one-id case of the same path.
101
- export async function evalTimelines(ids: readonly string[], ctx?: EvalContext): Promise<EvalTimeline[]> {
105
+ // @@@ `order` skips the FRESHNESS pass, not the rows - a caller that only needs to know WHICH scenarios are
106
+ // in play and in what sequence (identity, filed time, measured-or-blind) pays for none of the probes: on a
107
+ // 415-node scope the freshness pass is 25s and 1476 git children, the rows themselves are milliseconds.
108
+ // Rows returned this way carry `freshnessDeferred` INSTEAD of `fresh`/`staleAxes`, and evalReviewState
109
+ // refuses them loudly — an order row can never be mistaken for a measured verdict.
110
+ export async function evalTimelines(ids: readonly string[], ctx?: EvalContext, opts: { order?: boolean } = {}): Promise<EvalTimeline[]> {
102
111
  const root = ctx?.root ?? repoRoot()
103
112
  const ynodes = ctx?.ynodes ?? evalNodes(root)
104
113
  const specs = ctx?.specs ?? await loadSpecs()
@@ -123,13 +132,24 @@ export async function evalTimelines(ids: readonly string[], ctx?: EvalContext):
123
132
  return { id, ynode, codeEntries, rows, retractions, oks }
124
133
  })
125
134
 
126
- // An off-history anchor is the only reading that needs a content verdict; those primes serialize inside the
127
- // probe per anchor, so issuing them together lets one anchor's paths union into one child instead of N.
128
- await Promise.all(plans.flatMap((plan) => plan.ynode
135
+ // an order-only read stops HERE, before a single probe: the rows above are already the whole answer.
136
+ if (opts.order)
137
+ return plans.map((plan) => assembleTimeline(plan.id, plan.ynode, plan.rows, plan.retractions, plan.oks, {
138
+ idx, scidx, tracks, probe, anchors, order: true,
139
+ }))
140
+
141
+ // An off-history anchor is the only reading that needs a content verdict. Plan the population first so the
142
+ // probe can answer many anchor/HEAD pairs through bounded plural children instead of one child per anchor.
143
+ const contentDemands = plans.flatMap((plan) => plan.ynode
129
144
  ? plan.rows.filter((row) => !commitReachable(idx, row.reading.codeSha))
130
- .map((row) => probe.prime?.(row.reading.codeSha, row.axis.paths, plan.ynode!.evalPath))
131
- : []))
145
+ .map((row) => ({ anchorSha: row.reading.codeSha, paths: row.axis.paths, evalPath: plan.ynode!.evalPath }))
146
+ : [])
147
+ if (probe.primeMany) await probe.primeMany(contentDemands)
148
+ else await Promise.all(contentDemands.map((row) => probe.prime?.(row.anchorSha, [...row.paths], row.evalPath)))
132
149
  await anchors.prime?.(plans.flatMap((plan) => plan.rows.map((row) => ({ sinceSha: row.reading.codeSha, entries: row.axis.entries }))))
150
+ // the primes above only RECORDED which eval.md blocks the assemble step will compare; one batched pair of
151
+ // children answers the whole read's worth, the same shape the anchor prime above already has.
152
+ await probe.primeBlocks?.()
133
153
 
134
154
  return plans.map((plan) => assembleTimeline(plan.id, plan.ynode, plan.rows, plan.retractions, plan.oks, {
135
155
  idx, scidx, tracks, probe, anchors,
@@ -143,6 +163,7 @@ export async function evalTimeline(id: string, ctx?: EvalContext): Promise<EvalT
143
163
  type AssembleDeps = {
144
164
  idx: DriftIndex; scidx: ScenarioIndex; tracks: Map<string, RemarkTrack>
145
165
  probe: ReturnType<typeof contentProbeFor>; anchors: ReturnType<typeof anchorProbeFor>
166
+ order?: boolean // identity/sequence only — emit no freshness claim
146
167
  }
147
168
  function assembleTimeline(
148
169
  id: string,
@@ -150,7 +171,7 @@ function assembleTimeline(
150
171
  rows: { reading: Reading; axis: ReturnType<typeof scenarioCodeAxis> }[],
151
172
  retractions: Retraction[],
152
173
  oks: HumanOk[],
153
- { idx, scidx, tracks, probe, anchors }: AssembleDeps,
174
+ { idx, scidx, tracks, probe, anchors, order }: AssembleDeps,
154
175
  ): EvalTimeline {
155
176
  if (!ynode) return { node: id, hasEvalFile: false, scenarios: [], readings: [], retractions: [], dangling: [] }
156
177
  const byName = new Map(ynode.scenarios.map((s) => [s.name, s]))
@@ -164,9 +185,9 @@ function assembleTimeline(
164
185
  const readings: EvalEntry[] = []
165
186
  for (const { reading: r, axis } of rows) {
166
187
  const sc = byName.get(r.scenario)
167
- const axes = staleAxes(r, axis.entries, ynode.evalPath, idx, scidx,
188
+ const axes = order ? [] : staleAxes(r, axis.entries, ynode.evalPath, idx, scidx,
168
189
  remarksFor(r.scenario).map((rm) => ({ resolved: !!rm.resolved, resolvedAt: rm.resolvedAt })), probe, sc, anchors)
169
- const drift = axes.includes('code') ? codeDrift(idx, r.codeSha, axis.entries, probe) : []
190
+ const drift = !order && axes.includes('code') ? codeDrift(idx, r.codeSha, axis.entries, probe) : []
170
191
  const evidence: EvidenceView[] = evidenceOf(r).map((e) => ({ hash: e.hash, kind: e.kind, state: hasBlob(e.hash) ? 'present' : 'miss' }))
171
192
  const primary = evidence.find((e) => e.kind === 'video') ?? evidence[0]
172
193
  const okRow = humanOkFor(oks, r.scenario, r.ts)
@@ -182,8 +203,9 @@ function assembleTimeline(
182
203
  ...(r.by ? { by: r.by } : {}),
183
204
  ...(r.verdict ? { verdict: r.verdict } : {}),
184
205
  ts: r.ts,
185
- fresh: axes.length === 0,
206
+ fresh: !order && axes.length === 0,
186
207
  staleAxes: axes,
208
+ ...(order ? { freshnessDeferred: true as const } : {}),
187
209
  ...(drift.length ? { codeDrift: drift } : {}),
188
210
  blobState: primary ? primary.state : 'none',
189
211
  ...(threadFor(r.scenario) ? { thread: threadFor(r.scenario) } : {}),