spexcode 0.3.0 → 0.4.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/package.json +1 -1
- package/spec-cli/src/anchors.ts +300 -0
- package/spec-cli/src/cli.ts +11 -15
- package/spec-cli/src/git.ts +21 -13
- package/spec-cli/src/guide.ts +22 -5
- package/spec-cli/src/help.ts +19 -9
- package/spec-cli/src/index.ts +14 -0
- package/spec-cli/src/init.ts +5 -4
- package/spec-cli/src/lint.ts +55 -22
- package/spec-cli/src/localIssues.ts +19 -0
- package/spec-cli/src/migrate-table.ts +13 -12
- package/spec-cli/src/search.bench.mjs +2 -2
- package/spec-cli/src/specs.ts +19 -14
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/extract/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +20 -0
- package/spec-cli/templates/spec/project/.plugins/spec.md +5 -2
- package/spec-cli/templates/spec/project/.plugins/supervisor/spec.md +1 -1
- package/spec-cli/templates/spec/project/spec.md +1 -1
- package/spec-dashboard/dist/assets/{Dashboard-C7Bzsv86.js → Dashboard-CTcH2eW9.js} +3 -3
- package/spec-dashboard/dist/assets/EvalsPage-CJNKwHLN.js +2 -0
- package/spec-dashboard/dist/assets/{FoldToggle-D5iB4Ac2.js → FoldToggle-CVFbBpyW.js} +1 -1
- package/spec-dashboard/dist/assets/{IssuesPage-CMFTsQhg.js → IssuesPage-kULjonqj.js} +1 -1
- package/spec-dashboard/dist/assets/{MobileApp-DwuTKgdP.js → MobileApp-B0ZJju8K.js} +1 -1
- package/spec-dashboard/dist/assets/{SessionInterface-CBS5_cmK.js → SessionInterface-BRKJqU2U.js} +1 -1
- package/spec-dashboard/dist/assets/{SessionWindow-CqAnjWfI.js → SessionWindow-CDhEL7wO.js} +7 -7
- package/spec-dashboard/dist/assets/{Settings-BW5f0OaW.js → Settings-BL6FV_8S.js} +1 -1
- package/spec-dashboard/dist/assets/{index-Cc26X4ce.css → index-DmQsNYKK.css} +1 -1
- package/spec-dashboard/dist/assets/{index-Ce0wDyQS.js → index-DulGPk6A.js} +7 -7
- package/spec-dashboard/dist/index.html +2 -2
- package/spec-eval/src/cli.ts +50 -3
- package/spec-eval/src/evaltab.ts +11 -3
- package/spec-eval/src/humanok.ts +43 -0
- package/spec-eval/src/sidecar.ts +35 -9
- package/spec-cli/templates/presets/careful/.plugins/clarify-before-code/spec.md +0 -11
- package/spec-dashboard/dist/assets/EvalsPage-DKZZIdHq.js +0 -2
- /package/spec-cli/templates/spec/project/.plugins/{forge-link → prompts/forge-link}/spec.md +0 -0
- /package/spec-cli/templates/spec/project/.plugins/{memory-hygiene → prompts/memory-hygiene}/spec.md +0 -0
- /package/spec-cli/templates/spec/project/.plugins/{reproduce-before-fix → prompts/reproduce-before-fix}/spec.md +0 -0
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
} catch (e) { /* localStorage/matchMedia unavailable — default (light) stands */ }
|
|
20
20
|
})()
|
|
21
21
|
</script>
|
|
22
|
-
<script type="module" crossorigin src="/assets/index-
|
|
23
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
22
|
+
<script type="module" crossorigin src="/assets/index-DulGPk6A.js"></script>
|
|
23
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DmQsNYKK.css">
|
|
24
24
|
</head>
|
|
25
25
|
<body>
|
|
26
26
|
<div id="root"></div>
|
package/spec-eval/src/cli.ts
CHANGED
|
@@ -428,6 +428,51 @@ async function retractCmd(args: string[]): Promise<number> {
|
|
|
428
428
|
return 0
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
// `spex eval ok` — the HUMAN sign-off on a scenario's latest reading ([[human-ok]]): appends a monotonic
|
|
432
|
+
// human-ok event bound to that one immutable reading (a newer reading or live-computed staleness brings the
|
|
433
|
+
// scenario back on its own — no un-ok verb exists). CLI parity with the dashboard's affordance (LAW L);
|
|
434
|
+
// identity is the surface's: 'human' here — and a GOVERNED session is REFUSED, because the sign-off is the
|
|
435
|
+
// human's own deliberate judgment and an agent blessing its own reading would hide it from exactly the
|
|
436
|
+
// review the ok certifies (the no-self-resolve analogue). Flag set closed, like every eval verb.
|
|
437
|
+
async function okCmd(args: string[]): Promise<number> {
|
|
438
|
+
const root = repoRoot()
|
|
439
|
+
for (let i = 0; i < args.length; i++) {
|
|
440
|
+
const a = args[i]
|
|
441
|
+
if (!a.startsWith('--')) continue
|
|
442
|
+
if (a === '--scenario') { i++; continue }
|
|
443
|
+
console.error(`spex eval ok: unknown flag '${a}' — accepts --scenario`)
|
|
444
|
+
return 2
|
|
445
|
+
}
|
|
446
|
+
if (envSessionId()) {
|
|
447
|
+
console.error('spex eval ok: refusing under a governed session — human-ok is the HUMAN\'s sign-off, never an agent\'s self-blessing. Ask the human to ok it from the dashboard or their own terminal; an agent\'s judgment on a reading is a remark (`spex remark add`).')
|
|
448
|
+
return 1
|
|
449
|
+
}
|
|
450
|
+
const sel = positional(args)
|
|
451
|
+
if (!sel || sel === '.') { console.error('spex eval ok: name a node — usage: spex eval ok <node> --scenario <name>'); return 2 }
|
|
452
|
+
const node = stripRefSigil(sel)
|
|
453
|
+
const scName = flag(args, 'scenario')
|
|
454
|
+
const res = resolveEvalNode(evalNodes(root), node)
|
|
455
|
+
if (!res.ok) { console.error(`spex eval ok: ${res.error}`); return 1 }
|
|
456
|
+
const declared = res.node.scenarios.map((s) => s.name)
|
|
457
|
+
const scenario = scName ?? (declared.length === 1 ? declared[0] : undefined)
|
|
458
|
+
if (!scenario) {
|
|
459
|
+
console.error(`spex eval ok: '${res.node.id}' declares ${declared.length} scenarios — name one with --scenario <name> (declared: ${declared.join(', ')})`)
|
|
460
|
+
return 1
|
|
461
|
+
}
|
|
462
|
+
const { fileHumanOk } = await import('./humanok.js')
|
|
463
|
+
const r = fileHumanOk(res.node.id, scenario, 'human')
|
|
464
|
+
if (!r.ok) { console.error(`spex eval ok: ${r.error}`); return 1 }
|
|
465
|
+
if (r.already) {
|
|
466
|
+
console.log(`spex eval ok: '${res.node.id}' scenario '${scenario}' reading @ ${r.humanOk.okTs} is already human-ok'd (by ${r.humanOk.by}, ${r.humanOk.ts}) — monotonic, nothing appended`)
|
|
467
|
+
return 0
|
|
468
|
+
}
|
|
469
|
+
console.log(` ☑ '${res.node.id}' scenario '${scenario}' reading @ ${r.humanOk.okTs} (${r.humanOk.okSha.slice(0, 7)}) human-ok'd`)
|
|
470
|
+
console.log(r.landed === 'committed'
|
|
471
|
+
? 'spex eval ok: 1 sign-off filed (committed straight to the trunk)'
|
|
472
|
+
: 'spex eval ok: 1 sign-off filed (an appended event — commit the sidecar so the sign-off lands)')
|
|
473
|
+
return 0
|
|
474
|
+
}
|
|
475
|
+
|
|
431
476
|
async function clean(args: string[]): Promise<number> {
|
|
432
477
|
const root = repoRoot()
|
|
433
478
|
const all = has(args, 'all')
|
|
@@ -521,7 +566,8 @@ export function formatTimeline(tl: EvalTimeline): string {
|
|
|
521
566
|
const ev = list.length
|
|
522
567
|
? list.map((e) => e.state === 'miss' ? 'miss original file' : `${e.kind} ${(e.hash ?? '').slice(0, 12)}…`).join(', ')
|
|
523
568
|
: 'no evidence'
|
|
524
|
-
const
|
|
569
|
+
const okTag = r.humanOk ? ` ☑ human-ok (${r.humanOk.by})` : ''
|
|
570
|
+
const head = ` ${r.scenario.padEnd(w)} ${verdictText(r.verdict)} ${badge}${okTag} ${r.codeSha.slice(0, 7)} ${ev} ${r.ts}`
|
|
525
571
|
return r.expected ? [head, ` ${' '.repeat(w)} expected: ${r.expected}`] : [head]
|
|
526
572
|
})
|
|
527
573
|
return [`spex eval ls: '${tl.node}' — ${tl.readings.length} eval(s), newest first`, '', ...lines, ...retractLines].join('\n')
|
|
@@ -585,13 +631,14 @@ async function scenarioLs(args: string[]): Promise<number> {
|
|
|
585
631
|
|
|
586
632
|
// the `spex eval` drawer's node-scoped verbs ([[cli-surface]]): add (file a measurement) · ls (a node's
|
|
587
633
|
// reading timeline) · scenario ls (the declared contracts, --unmeasured = blind spots) · lint (the
|
|
588
|
-
// measurement-layer lint — advisory, always exit 0) · retract · clean.
|
|
634
|
+
// measurement-layer lint — advisory, always exit 0) · ok (the human sign-off) · retract · clean.
|
|
589
635
|
// The session-scoped read (`spex eval ls --session <SEL>`) is intercepted in cli.ts before this runs;
|
|
590
636
|
// `check-staged` is hook plumbing, exported separately for `spex internal check-staged`.
|
|
591
637
|
export async function runEval(args: string[]): Promise<number> {
|
|
592
638
|
const sub = args[0]
|
|
593
639
|
if (sub === 'lint') return scan(args.slice(1))
|
|
594
640
|
if (sub === 'add') return evalCmd(args.slice(1))
|
|
641
|
+
if (sub === 'ok') return okCmd(args.slice(1))
|
|
595
642
|
if (sub === 'retract') return retractCmd(args.slice(1))
|
|
596
643
|
if (sub === 'clean') return clean(args.slice(1))
|
|
597
644
|
if (sub === 'ls') return show(args.slice(1))
|
|
@@ -600,7 +647,7 @@ export async function runEval(args: string[]): Promise<number> {
|
|
|
600
647
|
console.error('spex eval scenario: ls [<node>|.] [--unmeasured] [--json] — list declared scenarios (the measurement contracts)')
|
|
601
648
|
return 2
|
|
602
649
|
}
|
|
603
|
-
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] | lint [--changed] | retract [.|<node>] [--scenario <name>] [--last | --ts <iso>] [--note <why>] | clean [--keep-latest|--all]')
|
|
650
|
+
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] | lint [--changed] | ok <node> [--scenario <name>] | retract [.|<node>] [--scenario <name>] [--last | --ts <iso>] [--note <why>] | clean [--keep-latest|--all]')
|
|
604
651
|
return 2
|
|
605
652
|
}
|
|
606
653
|
|
package/spec-eval/src/evaltab.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { repoRoot, driftIndex, historyIndex, type DriftIndex, type HistoryIndex
|
|
|
3
3
|
import { loadSpecs } from '../../spec-cli/src/specs.js'
|
|
4
4
|
import { loadEvalRemarkTracks, trackKey, type RemarkTrack, type Issue, type Reply } from '../../spec-cli/src/issues.js'
|
|
5
5
|
import { evalNodes, type EvalNode } from './scenarios.js'
|
|
6
|
-
import { readSidecar, applyRetractions, evidenceOf, isJsonBlob, type Verdict, type EvidenceKind, type Retraction } from './sidecar.js'
|
|
6
|
+
import { readSidecar, applyRetractions, evidenceOf, isJsonBlob, humanOkFor, type Verdict, type EvidenceKind, type Retraction } from './sidecar.js'
|
|
7
7
|
import { staleAxes, codeDrift, contentProbeFor, type StaleAxis } from './freshness.js'
|
|
8
8
|
import { scenarioIndex, type ScenarioIndex } from './scenariofresh.js'
|
|
9
9
|
import { hasBlob, getBlob, MISS_BLOB } from './cache.js'
|
|
@@ -64,6 +64,10 @@ export type EvalEntry = {
|
|
|
64
64
|
// so the eval detail pane reads its whole comment thread from the reading overlay — the counterpart to
|
|
65
65
|
// splitting eval-remark threads OUT of the issue surfaces (mergedIssues). Absent until the first remark.
|
|
66
66
|
thread?: Issue
|
|
67
|
+
// the human sign-off bound to THIS reading ([[human-ok]]) — present only on the exact reading an ok row
|
|
68
|
+
// anchors (by scenario + ts), so a newer reading arrives unblessed and the feed's hide releases itself.
|
|
69
|
+
// Rides the board fold verbatim (latestPerScenario is a filter, never a projection).
|
|
70
|
+
humanOk?: { by: string; ts: string }
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
// a remark overlaid onto its display host (a reading, above) → the RemarkView the surfaces read.
|
|
@@ -163,8 +167,8 @@ export async function evalTimeline(id: string, ctx?: EvalContext): Promise<EvalT
|
|
|
163
167
|
...(s.tags?.length ? { tags: s.tags } : {}), ...(s.code?.length ? { code: s.code } : {}),
|
|
164
168
|
}))
|
|
165
169
|
// one raw sidecar read: the effective readings feed the scoreboard rows below; the retraction events ride
|
|
166
|
-
// along as the undo trace (newest-first, like the readings).
|
|
167
|
-
const { readings: rawReadings, retractions } = readSidecar(ynode.sidecarPath)
|
|
170
|
+
// along as the undo trace (newest-first, like the readings), the human-ok events as the sign-off overlay.
|
|
171
|
+
const { readings: rawReadings, retractions, oks } = readSidecar(ynode.sidecarPath)
|
|
168
172
|
// the off-history content fallback ([[eval-core]]): fed to both git axes so a rebased/folded-away
|
|
169
173
|
// anchor with byte-identical governed content reads fresh. Lazy — an in-history reading never probes.
|
|
170
174
|
const probe = contentProbeFor(root)
|
|
@@ -183,6 +187,9 @@ export async function evalTimeline(id: string, ctx?: EvalContext): Promise<EvalT
|
|
|
183
187
|
// first) drives the scalar compat fields for single-evidence consumers.
|
|
184
188
|
const evidence: EvidenceView[] = evidenceOf(r).map((e) => ({ hash: e.hash, kind: e.kind, state: hasBlob(e.hash) ? 'present' : 'miss' }))
|
|
185
189
|
const primary = evidence.find((e) => e.kind === 'video') ?? evidence[0]
|
|
190
|
+
// the sign-off join ([[human-ok]]): the ok binds by exact (scenario, ts), so only the very reading the
|
|
191
|
+
// human blessed carries it — a newer or retract-revealed reading reads unblessed.
|
|
192
|
+
const okRow = humanOkFor(oks, r.scenario, r.ts)
|
|
186
193
|
return {
|
|
187
194
|
scenario: r.scenario,
|
|
188
195
|
expected: byName.get(r.scenario)?.expected ?? '',
|
|
@@ -200,6 +207,7 @@ export async function evalTimeline(id: string, ctx?: EvalContext): Promise<EvalT
|
|
|
200
207
|
...(drift.length ? { codeDrift: drift } : {}),
|
|
201
208
|
blobState: primary ? primary.state : 'none',
|
|
202
209
|
...(threadFor(r.scenario) ? { thread: threadFor(r.scenario) } : {}),
|
|
210
|
+
...(okRow ? { humanOk: { by: okRow.by, ts: okRow.ts } } : {}),
|
|
203
211
|
}
|
|
204
212
|
})
|
|
205
213
|
readings.reverse() // newest-first
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { relative } from 'node:path'
|
|
2
|
+
import { repoRoot } from '../../spec-cli/src/git.js'
|
|
3
|
+
import { commitTrunkData } from '../../spec-cli/src/localIssues.js'
|
|
4
|
+
import { evalNodes, resolveEvalNode } from './scenarios.js'
|
|
5
|
+
import { readReadings, readSidecar, appendHumanOk, humanOkFor, type HumanOk } from './sidecar.js'
|
|
6
|
+
|
|
7
|
+
// @@@ human-ok - the human sign-off on an eval reading ([[human-ok]]). One write, both surfaces: the CLI
|
|
8
|
+
// (`spex eval ok`) and the HTTP route (`POST /api/specs/:id/evals/ok`, identity server-derived 'human')
|
|
9
|
+
// call THIS. The ok binds to ONE immutable reading — the scenario's latest effective reading at ok-time,
|
|
10
|
+
// anchored by its (ts, codeSha) — and is MONOTONIC: no un-ok verb exists, because a newer reading is a
|
|
11
|
+
// different object the ok never transfers to, and staleness is computed live; both bring the scenario back
|
|
12
|
+
// on their own. Durability follows the checkout: on the trunk checkout the append is committed straight to
|
|
13
|
+
// trunk (`--no-verify`, path-scoped, under the shared store lock — the [[local-issues]] discipline,
|
|
14
|
+
// commitTrunkData); on a linked worktree the append stays in that tree and the session's own ritual commit
|
|
15
|
+
// carries it, exactly like every other sidecar write.
|
|
16
|
+
export type OkResult =
|
|
17
|
+
| { ok: true; humanOk: HumanOk; already: boolean; landed: 'committed' | 'uncommitted' }
|
|
18
|
+
| { ok: false; error: string }
|
|
19
|
+
|
|
20
|
+
export function fileHumanOk(nodeId: string, scenario: string, by: string): OkResult {
|
|
21
|
+
const root = repoRoot()
|
|
22
|
+
// the same loud resolution every eval verb applies ([[eval-core]]): exact canonical id, else a unique
|
|
23
|
+
// bare leaf; an ambiguous leaf returns the candidate list instead of blessing an arbitrary node.
|
|
24
|
+
const res = resolveEvalNode(evalNodes(root), nodeId)
|
|
25
|
+
if (!res.ok) return { ok: false, error: res.error }
|
|
26
|
+
const node = res.node
|
|
27
|
+
if (!node.scenarios.some((s) => s.name === scenario) &&
|
|
28
|
+
!readSidecar(node.sidecarPath).readings.some((r) => r.scenario === scenario))
|
|
29
|
+
return { ok: false, error: `'${node.id}' has no scenario '${scenario}'` }
|
|
30
|
+
// the ok's one possible target: the latest EFFECTIVE reading — an ok is a judgment on a measurement that
|
|
31
|
+
// exists and currently counts; an unmeasured (or fully-retracted) scenario has nothing to bless.
|
|
32
|
+
const forScenario = readReadings(node.sidecarPath).filter((r) => r.scenario === scenario)
|
|
33
|
+
if (!forScenario.length) return { ok: false, error: `'${node.id}' scenario '${scenario}' has no effective reading — nothing to ok` }
|
|
34
|
+
const latest = forScenario[forScenario.length - 1]
|
|
35
|
+
// a duplicate ok is idempotent success (the store already IS the requested state — the local-issue
|
|
36
|
+
// close's `already` semantics), never an error and never a second appended row.
|
|
37
|
+
const existing = humanOkFor(readSidecar(node.sidecarPath).oks, scenario, latest.ts)
|
|
38
|
+
if (existing) return { ok: true, humanOk: existing, already: true, landed: 'committed' }
|
|
39
|
+
const row: HumanOk = { kind: 'human-ok', scenario, okTs: latest.ts, okSha: latest.codeSha, by, ts: new Date().toISOString() }
|
|
40
|
+
appendHumanOk(node.sidecarPath, row)
|
|
41
|
+
const landed = commitTrunkData(relative(root, node.sidecarPath), `eval(${node.id}): human-ok '${scenario}' @ ${latest.ts} by ${by}`)
|
|
42
|
+
return { ok: true, humanOk: row, already: false, landed: landed === 'not-primary' ? 'uncommitted' : 'committed' }
|
|
43
|
+
}
|
package/spec-eval/src/sidecar.ts
CHANGED
|
@@ -72,18 +72,28 @@ export function isJsonBlob(b: Buffer): boolean {
|
|
|
72
72
|
|
|
73
73
|
// a RETRACTION is the sanctioned inverse of a filing — itself an appended event, never a deleted line
|
|
74
74
|
// (the sidecar stays append-only; git shows who retracted what, when). `retracts` is the target reading's
|
|
75
|
-
// `ts` within `scenario` (its natural key). The
|
|
76
|
-
// carries `retracts`, a reading carries `codeSha
|
|
77
|
-
// is the retracting session; `note` says why (a botched e2e
|
|
75
|
+
// `ts` within `scenario` (its natural key). The event kinds are told apart POSITIVELY — a retraction
|
|
76
|
+
// carries `retracts`, a reading carries `codeSha`, a human-ok carries `kind: 'human-ok'`; none is ever
|
|
77
|
+
// recognized by another field's absence. `by` is the retracting session; `note` says why (a botched e2e
|
|
78
|
+
// filing, a wrong verdict).
|
|
78
79
|
export type Retraction = { retracts: string; scenario: string; note?: string; by?: string; ts: string }
|
|
79
80
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
81
|
+
// a HUMAN-OK ([[human-ok]]) is the human's sign-off on ONE immutable reading — an appended event like the
|
|
82
|
+
// others, never a mutation. `okTs`+`okSha` anchor the blessed reading (its ts is the natural key within
|
|
83
|
+
// `scenario`, exactly retraction's join; the sha rides for the human reader). The ok is MONOTONIC — there
|
|
84
|
+
// is no un-ok event: a newer reading is a different object the ok never transfers to, and staleness is
|
|
85
|
+
// computed live, so both automatically bring the scenario back. A pre-human-ok toolchain skips these lines
|
|
86
|
+
// silently (no top-level `codeSha`, so its reading parse never claims them).
|
|
87
|
+
export type HumanOk = { kind: 'human-ok'; scenario: string; okTs: string; okSha: string; by: string; ts: string }
|
|
88
|
+
|
|
89
|
+
// parse the sidecar RAW: one event per non-blank line — a Reading, a Retraction (a line carrying a string
|
|
90
|
+
// `retracts`), or a HumanOk (kind 'human-ok'). A malformed line is skipped (the file is append-only and
|
|
91
|
+
// git-tracked, so a partial write or a hand-edit shouldn't sink the whole read) — fail soft per line.
|
|
92
|
+
export function readSidecar(sidecarPath: string): { readings: Reading[]; retractions: Retraction[]; oks: HumanOk[] } {
|
|
84
93
|
const readings: Reading[] = []
|
|
85
94
|
const retractions: Retraction[] = []
|
|
86
|
-
|
|
95
|
+
const oks: HumanOk[] = []
|
|
96
|
+
if (!existsSync(sidecarPath)) return { readings, retractions, oks }
|
|
87
97
|
for (const line of readFileSync(sidecarPath, 'utf8').split('\n')) {
|
|
88
98
|
const t = line.trim()
|
|
89
99
|
if (!t) continue
|
|
@@ -91,10 +101,11 @@ export function readSidecar(sidecarPath: string): { readings: Reading[]; retract
|
|
|
91
101
|
const r = JSON.parse(t)
|
|
92
102
|
if (!r || typeof r.scenario !== 'string') continue
|
|
93
103
|
if (typeof r.retracts === 'string') retractions.push(r as Retraction)
|
|
104
|
+
else if (r.kind === 'human-ok' && typeof r.okTs === 'string') oks.push(r as HumanOk)
|
|
94
105
|
else if (typeof r.codeSha === 'string') readings.push(r as Reading)
|
|
95
106
|
} catch { /* skip a malformed line */ }
|
|
96
107
|
}
|
|
97
|
-
return { readings, retractions }
|
|
108
|
+
return { readings, retractions, oks }
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
// the retraction join, shared by every effective-view reader: drop each reading a retraction targets by
|
|
@@ -127,6 +138,21 @@ export function appendRetraction(sidecarPath: string, r: Retraction): void {
|
|
|
127
138
|
appendFileSync(sidecarPath, JSON.stringify(r) + '\n')
|
|
128
139
|
}
|
|
129
140
|
|
|
141
|
+
// append ONE human-ok as a JSON line — the sign-off writes through the same append-only surface; the
|
|
142
|
+
// blessed reading stays untouched, the ok binds to it by (scenario, okTs).
|
|
143
|
+
export function appendHumanOk(sidecarPath: string, r: HumanOk): void {
|
|
144
|
+
appendFileSync(sidecarPath, JSON.stringify(r) + '\n')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// the ok that binds to a reading — the LAST ok row targeting (scenario, ts), or null. An ok anchored to a
|
|
148
|
+
// retracted/superseded reading is inert history: it binds to nothing current, so the join is by exact
|
|
149
|
+
// (scenario, okTs) against whichever readings the caller passes.
|
|
150
|
+
export function humanOkFor(oks: HumanOk[], scenario: string, readingTs: string): HumanOk | null {
|
|
151
|
+
let hit: HumanOk | null = null
|
|
152
|
+
for (const o of oks) if (o.scenario === scenario && o.okTs === readingTs) hit = o
|
|
153
|
+
return hit
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
// the latest reading per scenario (the file is chronological, so the LAST line for a name wins). clean's
|
|
131
157
|
// --keep-latest uses it to decide which blob to keep.
|
|
132
158
|
export function latestPerScenario(readings: Reading[]): Map<string, Reading> {
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: clarify-before-code
|
|
3
|
-
surface: system
|
|
4
|
-
status: active
|
|
5
|
-
hue: 30
|
|
6
|
-
desc: A config plugin (careful preset) — before coding, the worker surfaces ambiguities as explicit assumptions in its proposal, and blocks for a live question only on a load-bearing one.
|
|
7
|
-
code:
|
|
8
|
-
---
|
|
9
|
-
Before you write code, enumerate the ambiguities, contradictions, and technical risks in your task. Most of them are cheap: resolve a cheap one by **stating your assumption explicitly** in the work you propose. SpexCode's manager-merge review is already the hard gate — a misread surfaces there, in the proposal, not in a live interrogation of the human. So clarification shifts **left into the artifact**: the diff and the spec body say what you assumed, and the reviewer catches a wrong assumption at merge.
|
|
10
|
-
|
|
11
|
-
Block for a live question (the `needs-input` channel) **only on a load-bearing ambiguity** — one where guessing wrong would waste the whole node. A small or clear task proceeds without asking. This is deliberately the opposite of "every agent asks the user": the default is to proceed on a stated assumption, and the human is interrupted only when proceeding blind would burn the work.
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{u as pe,r as a,s as We,e as Ye,j as n,f as Ge}from"./index-Ce0wDyQS.js";import{V as Ze,W as Je,X as Qe,Y as et,Z as tt,g as fe,n as nt,_ as Q,$ as st,a0 as Ae,a1 as at,a2 as lt,u as ot,z as he,a3 as it,o as rt,a4 as ct}from"./SessionWindow-CqAnjWfI.js";import{F as me}from"./FoldToggle-D5iB4Ac2.js";const Ie=(l,c)=>{let d=null;for(const v of l)if(v.at<=c)d=v;else break;return d},ut=l=>{if(!l||!Array.isArray(l.events))return null;const c=l.v===1?"time":typeof l.axis=="string"?l.axis:"time",d=l.v===1?v=>v.tMs:v=>v.at;return{axis:c,events:l.events.map(v=>({at:d(v),step:v.step,...v.node?{node:v.node}:{}}))}},dt=(l,c,d)=>l==="time"?Q(c):l==="frame"?`#${c}`:l==="line"?`L${c}`:l==="index"?d?`${c}/${d}`:`${c}`:String(c);function Pe({events:l,axis:c,extent:d,activeStepIdx:v,onSeek:$}){return n.jsx("div",{className:"an-ruler",children:l.map((i,f)=>n.jsxs("button",{className:`an-step ${v===f?"on":""}`,onClick:()=>$(i.at),"data-tip":i.node?`→ ${i.node}`:void 0,children:[dt(c,i.at,d)," ",i.step]},f))})}const ve=l=>{var c,d;return((c=l.verdict)==null?void 0:c.status)==="pass"?"✓":((d=l.verdict)==null?void 0:d.status)==="fail"?"✗":"·"},Fe=l=>{var c,d;return((c=l.verdict)==null?void 0:c.status)==="pass"?"pass":((d=l.verdict)==null?void 0:d.status)==="fail"?"fail":"legacy"};function ft({entry:l,specs:c=[],sessions:d=[],onWrite:v,onOpenSession:$}){var Re,Le,Te,De,Ce;const i=pe(),f=a.useRef(null),x=a.useRef(null),j=a.useRef(null),k=a.useRef(null),g=a.useRef(0),[u,M]=a.useState([]),[w,y]=a.useState("time"),[r,b]=a.useState(null),[S,T]=a.useState(""),[D,X]=a.useState(!1),[ee,z]=a.useState(null),C=a.useRef(null),h=a.useRef(null),I=a.useRef(null),te=a.useRef(0),[Be,xe]=a.useState(0),[ne,ge]=a.useState(-1),[ye,be]=a.useState(null),[H,se]=a.useState(!1),[we,ae]=a.useState(!1),[le,oe]=a.useState(null),[_,ie]=a.useState(null),[E,je]=a.useState(null),[A,q]=a.useState(0),p=E&&E[A]||l;a.useEffect(()=>{b(null),T(""),M([]),z(null),q(0),je(null);let e=!0;return fetch(We(l.node,"evals")).then(t=>t.ok?t.json():null).then(t=>{e&&Array.isArray(t==null?void 0:t.readings)&&je(t.readings.filter(s=>s.scenario===l.scenario))}).catch(()=>{}),()=>{e=!1}},[l.node,l.scenario,l.ts,l.blob]),a.useEffect(()=>{b(null),z(null)},[A]);const O=Ze(p),P=O.find(e=>e.kind==="video"&&e.state==="present"),re=O.filter(e=>e.kind==="image"),Ve=O.filter(e=>e.kind!=="video"&&e.kind!=="image"),K=!!P,B=l.thread??null,ce=a.useMemo(()=>B?[{by:B.by,at:B.created,body:B.body},...B.replies||[]]:[],[B]),Xe=E&&((Re=E[0])==null?void 0:Re.by)||l.by||null,R=a.useMemo(()=>ce.map((e,t)=>{const s=Je(Qe(e.body),u);return s&&s.seekable?{i:t,tMs:s.tMs,step:s.step,label:s.label}:null}).filter(Boolean).sort((e,t)=>e.tMs-t.tMs),[ce,u]),ke=a.useRef(u);ke.current=u;const Me=a.useRef(R);Me.current=R;const W=a.useCallback(()=>{const e=te.current,t=ke.current;let s=-1;for(let m=0;m<t.length&&t[m].at<=e;m++)s=m;ge(m=>m===s?m:s);let o=null;for(const m of Me.current)if(m.tMs<=e)o=m.i;else break;be(m=>m===o?m:o)},[]);a.useEffect(()=>{W()},[R,u,W]),a.useEffect(()=>{te.current=0,C.current&&(C.current.style.width="0%"),h.current&&(h.current.style.left="0%"),I.current&&(I.current.textContent=""),ge(-1),be(null),xe(0),se(!1),ae(!1),oe(null),ie(null)},[l.blob,l.scenario,l.node]),a.useEffect(()=>{if(M([]),y("time"),!p.timelineBlob)return;let e=!0;return fetch(`/api/evidence/${p.timelineBlob}`).then(t=>t.ok?t.json():null).then(t=>{const s=e&&ut(t);s&&(M(s.events),y(s.axis))}).catch(()=>{}),()=>{e=!1}},[p.timelineBlob]),a.useEffect(()=>{const e=f.current;if(!e)return;const t=()=>{const N=Math.round((e.currentTime||0)*1e3),L=Math.round((e.duration||0)*1e3);te.current=N;const J=L?N/L*100:0;C.current&&(C.current.style.width=`${J}%`),h.current&&(h.current.style.left=`${J}%`),I.current&&(I.current.textContent=`${Q(N)} / ${Q(L)}`),W()},s=()=>{xe(e.duration||0),t()},o=()=>se(!0),m=()=>se(!1);return e.addEventListener("timeupdate",t),e.addEventListener("seeked",t),e.addEventListener("loadedmetadata",s),e.addEventListener("durationchange",s),e.addEventListener("play",o),e.addEventListener("pause",m),s(),()=>{e.removeEventListener("timeupdate",t),e.removeEventListener("seeked",t),e.removeEventListener("loadedmetadata",s),e.removeEventListener("durationchange",s),e.removeEventListener("play",o),e.removeEventListener("pause",m)}},[P==null?void 0:P.hash,l.node,l.scenario,A,W]);const F=Math.round(Be*1e3),Y=u[ne]||null,Ne=w==="time"?F:w==="frame"?re.length:w==="index"?u.length:0,V=a.useCallback(e=>{const t=f.current;t&&(t.currentTime=e/1e3)},[]),G=a.useCallback(()=>{const e=f.current;e&&(e.paused?e.play():e.pause())},[]),Ee=(e,t)=>{ie(e),t!=null&&V(t)},Z=a.useCallback(async e=>{const t=f.current;if(!(t!=null&&t.videoWidth))return null;X(!0),T(i("annotator.capturing"));try{t.seeking&&await new Promise(L=>t.addEventListener("seeked",L,{once:!0}));const s=document.createElement("canvas");s.width=t.videoWidth,s.height=t.videoHeight;const o=s.getContext("2d");o.drawImage(t,0,0,s.width,s.height),e&&(o.strokeStyle="#ff9a3c",o.lineWidth=Math.max(2,s.width/300),o.strokeRect(e.x/100*s.width,e.y/100*s.height,e.w/100*s.width,e.h/100*s.height));const m=await new Promise(L=>s.toBlob(L,"image/png")),{hash:N}=await Ye(m);if(!N)throw new Error("no hash");return T(""),N}catch{return T(i("annotator.failed")),null}finally{X(!1)}},[i]),ze=a.useCallback(async()=>{var s;const e=f.current;if(!e)return null;e.pause();const t=Math.round((e.currentTime??0)*1e3);return{tMs:t,step:((s=Ie(u,t))==null?void 0:s.step)??null,frame:await Z(null)}},[u,Z]),ue=a.useCallback(async e=>{const t=f.current;if(!t)return;t.pause();const s=Math.round((t.currentTime||0)*1e3),o=Ie(u,s),m=await Z(e),N=[et(s,o==null?void 0:o.step)];m&&N.push(``),o!=null&&o.node&&o.node!==l.node&&N.push(`re: [[${o.node}]]`),N.push(""),z({seq:++g.current,body:N.join(`
|
|
2
|
-
`)})},[u,l.node,Z]),de=a.useCallback(e=>{var o;if(!R.length)return;let t=R.findIndex(m=>m.i===_);if(t<0){const m=Math.round((((o=f.current)==null?void 0:o.currentTime)||0)*1e3);t=R.reduce((N,L,J)=>L.tMs<=m?J:N,-1)}t=Math.min(R.length-1,Math.max(0,t+e));const s=R[t];ie(s.i),V(s.tMs)},[R,_,V]);a.useEffect(()=>{if(!K)return;const e=t=>{const s=document.activeElement;if(s&&(s.tagName==="INPUT"||s.tagName==="TEXTAREA"||s.tagName==="SELECT"||s.isContentEditable))return;const o=f.current;o&&(t.key===" "?(t.preventDefault(),G()):t.key==="ArrowRight"?(t.preventDefault(),o.currentTime=Math.min(o.duration||o.currentTime,o.currentTime+(t.shiftKey?1:5))):t.key==="ArrowLeft"?(t.preventDefault(),o.currentTime=Math.max(0,o.currentTime-(t.shiftKey?1:5))):t.key===","?(t.preventDefault(),o.currentTime=Math.max(0,o.currentTime-1/30)):t.key==="."?(t.preventDefault(),o.duration&&(o.currentTime=Math.min(o.duration,o.currentTime+1/30))):t.key==="ArrowDown"?(t.preventDefault(),de(1)):t.key==="ArrowUp"?(t.preventDefault(),de(-1)):(t.key==="a"||t.key==="A")&&(t.preventDefault(),ue(null)))};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[K,G,de,ue]);const $e=e=>{var o;const t=(o=j.current)==null?void 0:o.getBoundingClientRect(),s=f.current;!t||!t.width||!s||!s.duration||(s.currentTime=Math.min(1,Math.max(0,(e-t.left)/t.width))*s.duration)},Oe=e=>{e.preventDefault(),ae(!0),$e(e.clientX)},Ue=e=>{var s;const t=(s=j.current)==null?void 0:s.getBoundingClientRect();t!=null&&t.width&&oe(Math.min(100,Math.max(0,(e.clientX-t.left)/t.width*100)))};a.useEffect(()=>{if(!we)return;const e=s=>$e(s.clientX),t=()=>ae(!1);return window.addEventListener("mousemove",e),window.addEventListener("mouseup",t),()=>{window.removeEventListener("mousemove",e),window.removeEventListener("mouseup",t)}},[we]);const Se=e=>{const t=x.current.getBoundingClientRect();return{x:(e.clientX-t.left)/t.width*100,y:(e.clientY-t.top)/t.height*100}},He=e=>{if(e.button!==0||D)return;const t=Se(e);b({x0:t.x,y0:t.y,x:t.x,y:t.y})},_e=e=>{var s,o;if(!r)return;const t=Se(e);!((s=f.current)!=null&&s.paused)&&(Math.abs(t.x-r.x0)>1||Math.abs(t.y-r.y0)>1)&&((o=f.current)==null||o.pause()),b({...r,x:t.x,y:t.y})},qe=()=>{if(!r)return;const e={x:Math.min(r.x0,r.x),y:Math.min(r.y0,r.y),w:Math.abs(r.x-r.x0),h:Math.abs(r.y-r.y0)};if(b(null),e.w<1&&e.h<1){G();return}ue(e)},U=r&&{x:Math.min(r.x0,r.x),y:Math.min(r.y0,r.y),w:Math.abs(r.x-r.x0),h:Math.abs(r.y-r.y0)};return n.jsxs("div",{className:"an-detail",children:[n.jsxs("header",{className:"an-head",children:[n.jsx("span",{className:"an-title",children:l.scenario}),n.jsx("span",{className:"an-node",children:l.node}),n.jsx("span",{className:`an-verdict-badge ${Fe(p)}`,children:ve(p)}),p.evaluator&&n.jsx("span",{className:"an-meta",children:p.evaluator}),n.jsx("span",{className:"an-meta",children:new Date(p.ts).toLocaleString()}),n.jsx(tt,{originator:Xe,sessions:d,kind:"eval",onOpenSession:$}),E&&E.length>1&&n.jsxs("div",{className:"an-ab",children:[n.jsx(fe,{icon:"chevron-left",size:13,className:"an-ab-nav",disabled:A>=E.length-1,onClick:()=>q(e=>Math.min(E.length-1,e+1)),label:i("annotator.abOlder")}),n.jsx("div",{className:"an-ab-track",children:E.slice().reverse().map((e,t)=>{const s=E.length-1-t;return n.jsx("button",{type:"button",className:`an-ab-pip ${Fe(e)} ${s===A?"on":""}`,onClick:()=>q(s),"data-tip":`${ve(e)} ${new Date(e.ts).toLocaleString()}`,children:ve(e)},`${e.ts}-${s}`)})}),n.jsx(fe,{icon:"chevron-right",size:13,className:"an-ab-nav",disabled:A<=0,onClick:()=>q(e=>Math.max(0,e-1)),label:i("annotator.abNewer")}),n.jsx("span",{className:"an-ab-pos",children:A===0?i("annotator.abLatest"):i("annotator.abPos",{i:E.length-A,n:E.length})})]})]}),n.jsxs("div",{className:"an-work",children:[n.jsxs("div",{className:"an-stage-col",children:[p.expected&&n.jsxs("div",{className:"an-expected",children:[n.jsx("b",{children:i("nodeView.eval.expected")})," ",p.expected]}),O.length>0&&((Le=p.verdict)==null?void 0:Le.note)&&n.jsxs("div",{className:"an-expected an-prior-note",children:[n.jsx("b",{children:i("nodeView.eval.noteLabel")})," ",p.verdict.note]}),!p.fresh&&(((Te=p.staleAxes)==null?void 0:Te.length)??0)>0&&n.jsxs("div",{className:"an-expected an-stale","data-tip":i("nodeView.eval.staleReadoutTitle"),children:[n.jsx("b",{children:i("nodeView.eval.staleLabel")})," ",p.staleAxes.join(" · "),(((De=p.codeDrift)==null?void 0:De.length)??0)>0&&n.jsxs("span",{className:"an-stale-files",children:[" — ",p.codeDrift.map(e=>`${e.file.split("/").pop()} +${e.behind}`).join(", ")]})]}),P&&n.jsxs(n.Fragment,{children:[n.jsxs("div",{className:"an-player",ref:k,children:[n.jsxs("div",{className:`an-stage ${H?"playing":"paused"}`,ref:x,onMouseDown:He,onMouseMove:_e,onMouseUp:qe,children:[n.jsx("video",{className:"an-video",ref:f,src:`/api/evidence/${P.hash}`,preload:"metadata",playsInline:!0}),U&&n.jsx("div",{className:"an-rect live",style:{left:`${U.x}%`,top:`${U.y}%`,width:`${U.w}%`,height:`${U.h}%`}}),!H&&!r&&n.jsx("div",{className:"an-bigplay","aria-hidden":!0,children:n.jsx(nt,{name:"play",size:22})})]}),n.jsxs("div",{className:"an-bar",children:[n.jsx(fe,{icon:H?"pause":"play",size:14,className:"an-play",label:i(H?"annotator.pause":"annotator.play"),onClick:G}),n.jsxs("div",{className:"an-seek",ref:j,onMouseDown:Oe,onMouseMove:Ue,onMouseLeave:()=>oe(null),children:[n.jsx("div",{className:"an-seek-trk"}),F>0&&w==="time"&&u.map((e,t)=>n.jsx("div",{className:"an-band",style:{left:`${e.at/F*100}%`},"data-tip":e.step},`band-${t}`)),n.jsx("div",{className:"an-seek-play",ref:C}),F>0&&R.map(e=>n.jsx("button",{type:"button",className:`an-mk ${_===e.i?"on":""} ${ye===e.i?"active":""}`,style:{left:`${e.tMs/F*100}%`},"data-tip":e.label,onMouseDown:t=>t.stopPropagation(),onClick:t=>{t.stopPropagation(),Ee(e.i,e.tMs)}},`mk-${e.i}`)),n.jsx("div",{className:"an-knob",ref:h}),le!=null&&F>0&&n.jsx("div",{className:"an-seek-hov",style:{left:`${le}%`},children:Q(le/100*F)})]}),n.jsx("span",{className:"an-time",ref:I}),Y&&n.jsx("span",{className:"an-curstep","data-tip":Y.node?`→ ${Y.node}`:void 0,children:Y.step}),n.jsx(st,{target:k})]})]}),u.length>0&&n.jsx(Pe,{events:u,axis:w,extent:Ne,activeStepIdx:ne,onSeek:V}),n.jsx("div",{className:"an-hint",children:i("annotator.hint")}),n.jsx("div",{className:"an-keys",children:i("annotator.keys")}),S&&n.jsx("div",{className:"an-flash",children:S})]}),!P&&u.length>0&&n.jsx(Pe,{events:u,axis:w,extent:Ne,activeStepIdx:ne,onSeek:V}),re.length>0&&n.jsx("div",{className:"an-gallery",children:re.map((e,t)=>n.jsx(Ae,{e,alt:l.scenario},`${e.hash}-${t}`))}),Ve.map((e,t)=>n.jsx(Ae,{e,alt:l.scenario},`${e.hash}-${t}`)),O.length===0&&((Ce=p.verdict)!=null&&Ce.note?n.jsx("pre",{className:"eval-transcript",children:p.verdict.note}):n.jsx("div",{className:"an-hint",children:i("nodeView.eval.noImage")}))]}),n.jsx(ht,{entry:l,comments:ce,specs:c,sessions:d,onWrite:v,codeSha:p.codeSha,seekMs:K?V:null,anchorNow:K?ze:null,draft:ee,selIdx:_,activeIdx:ye,onSelect:K?Ee:null,events:K?u:null})]})]})}function ht({entry:l,comments:c,codeSha:d,specs:v,sessions:$,onWrite:i,seekMs:f,anchorNow:x,draft:j,selIdx:k,activeIdx:g,onSelect:u,events:M}){var r;const w=pe(),y=(b,S)=>Ge({node:l.node,scenario:l.scenario,body:b,codeSha:d,evidence:S});return n.jsxs("aside",{className:"an-rail",children:[n.jsx("div",{className:"an-comments-head",children:w("annotator.comments",{n:c.length})}),n.jsx("div",{className:"an-rail-list",children:n.jsx(at,{replies:c,onSeek:f,selIdx:k,activeIdx:g,onSelect:u,events:M,threadId:((r=l.thread)==null?void 0:r.id)??null,onRemarkChange:()=>i==null?void 0:i("")})}),n.jsx("div",{className:"an-rail-compose",children:n.jsx(lt,{onSend:y,specs:v,sessions:$,focusId:l.node,onDone:i,anchorNow:x,draft:j},`${l.node}·${l.scenario}`)})]})}function Ke({rowKeys:l,sel:c,onSel:d,detail:v,children:$}){const[i,f]=a.useState(!1),x=a.useRef({});x.current={rowKeys:l,sel:c},a.useEffect(()=>{const k=g=>{var b;const u=(b=g.target)==null?void 0:b.tagName;if(u==="INPUT"||u==="TEXTAREA"||g.metaKey||g.ctrlKey||g.altKey||g.key!=="j"&&g.key!=="k")return;g.preventDefault(),g.stopPropagation();const{rowKeys:M,sel:w}=x.current;if(!M.length)return;const y=M.indexOf(w),r=y<0?g.key==="j"?0:M.length-1:Math.max(0,Math.min(M.length-1,y+(g.key==="j"?1:-1)));d(M[r])};return window.addEventListener("keydown",k,!0),()=>window.removeEventListener("keydown",k,!0)},[d]),a.useEffect(()=>{var k;(k=document.querySelector(".fv-list-col .sel"))==null||k.scrollIntoView({block:"nearest"})},[c]);const j=n.jsx(me,{className:"fv-fold-inline",onToggle:()=>f(!0)});return n.jsxs("div",{className:`fv-master ${i?"folded":""}`,children:[i&&n.jsx(me,{className:"fv-unfold",folded:!0,onToggle:()=>f(!1)}),n.jsx("div",{className:"fv-list-col",style:i?{display:"none"}:void 0,children:typeof $=="function"?$(j):n.jsxs(n.Fragment,{children:[n.jsx(me,{className:"fv-fold",onToggle:()=>f(!0)}),$]})}),n.jsx("div",{className:"fv-detail",children:v})]})}function mt({specs:l=[],sessions:c=[],reloadBoard:d,onOpenSession:v}){const $=pe(),{page:i,param:f}=ot(),[x,j]=a.useState(null),[k,g]=a.useState(""),[u,M]=a.useState([]),w=a.useRef([]),y=a.useMemo(()=>new Map(u.map(h=>[he(h),h])),[u]);w.current=u.map(he);const r=x&&y.has(x)?x:w.current[0]??null,b=a.useMemo(()=>{if(!f)return null;const h=f.indexOf("/");return h>0?`eval:${f.slice(0,h)}·${f.slice(h+1)}`:null},[f]),[S,T]=a.useState(null);a.useEffect(()=>{i==="evals"&&b&&(j(b),T(b))},[i,b]),a.useEffect(()=>{S&&y.has(S)&&T(null)},[S,y]);const D=x&&!y.has(x),X=a.useMemo(()=>!D||it(l).some(h=>he(h)===x),[D,l,x]);a.useEffect(()=>{D&&!X&&(j(null),T(null))},[D,X]),a.useEffect(()=>{x&&!S&&u.length&&!y.has(x)&&j(null)},[x,S,u,y]),a.useEffect(()=>{if(i!=="evals"||!r||D)return;const h=/^eval:([^·]+)·(.+)$/.exec(r);h&&rt("evals",`${h[1]}/${h[2]}`,{replace:!0})},[i,r,D]);const ee=a.useCallback(h=>M(h),[]),z=h=>{h&&(g(h),setTimeout(()=>g(""),6e3))},C=r?y.get(r):null;return n.jsx(Ke,{rowKeys:w.current,sel:r,onSel:j,detail:C?n.jsx(ft,{entry:C,specs:l,sessions:c,onOpenSession:v,onWrite:async h=>{z(h),await(d==null?void 0:d())}}):n.jsx("div",{className:"fv-note",children:$("evalsFeed.empty")}),children:h=>n.jsxs(n.Fragment,{children:[k&&n.jsx("div",{className:"fv-notice",children:k}),n.jsx(ct,{nodes:l,sessions:c,sel:r,onSel:I=>j(I),onRows:ee,mustShow:S,lead:h})]})})}const gt=Object.freeze(Object.defineProperty({__proto__:null,EvalMasterDetail:Ke,default:mt},Symbol.toStringTag,{value:"Module"}));export{ft as E,Ke as a,gt as b};
|
|
File without changes
|
/package/spec-cli/templates/spec/project/.plugins/{memory-hygiene → prompts/memory-hygiene}/spec.md
RENAMED
|
File without changes
|
|
File without changes
|