diff --git a/examples/desktop/docs/integration-final-check.md b/examples/desktop/docs/integration-final-check.md new file mode 100644 index 0000000000..f4217d3df2 --- /dev/null +++ b/examples/desktop/docs/integration-final-check.md @@ -0,0 +1,99 @@ +# Integration final check — post-triple-merge + +- **Mainline HEAD**: `08837bd96717fcc7f63e38d7f6918d35f13bec00` + - `merge fix/usability-p0-batch @ 28e4f36 — usability P0 wiring: rubric fixture ids, cell-jump listener, artifact seed, empty panel` + - Parent chain: usability 08837bd ← nav-optional de2f812 ← visual-fix + stability batch. +- **Tests**: 1813/1813 pass (baseline 1789 pre-merge cold clone + 24 usability additions = 1813; static gates four-pack all green). +- **Timestamp**: 2026-07-19 +- **Gate agent**: qa-gate-6, isolated Electron on port 9445 with `--user-data-dir=/tmp/dsh-integration-final-userdata` + `DSH_DESKTOP_HOME=/tmp/dsh-integration-final-home`. User's daily Electron instances untouched. + +## 1. Full test — **PASS** + +``` +$ LEFTHOOK=0 node --test test/*.test.js +# tests 1813 +# pass 1813 +# fail 0 +``` + +## 2. Static gates four-pack — **PASS** + +``` +$ LEFTHOOK=0 node --test test/style-css-static.test.js test/emoji-ban-static.test.js test/renderer-collisions.test.js +# tests 12 +# pass 12 +# fail 0 +``` + +Gates covered: +- **brace-balance** — `style-css-static.test.js::brace balance — style.css blocks close cleanly` PASS. +- **centered-card ban** — `style-css-static.test.js::centered-card ban — no margin:auto / align-self:center on card family` PASS (density-spec §7). +- **emoji-ban** — `emoji-ban-static.test.js` PASS (shipped code paths only). +- **css-collision / renderer-collisions** — `renderer-collisions.test.js` PASS. + +## 3. Fresh Electron integration smoke — **PASS** + +Cold-boot on port 9445 with fresh userdata + isolated DSH_DESKTOP_HOME. Existing Electrons (ports 9227, 9271) left untouched. + +### ① Console zero SyntaxError / zero uncaught +``` +$ node /tmp/dsh-integration-smoke.mjs errors 4000 +{ "consoleErrors": [], "exceptions": [] } +``` + +### ② 14+ pages page-by-page no whitescreen +Setting `hiddenPages=[]` reveals all 14 nav items; each `switchTo` reaches a non-empty pane (min visibleTextLen=584 chars for playground-shim redirect stub, max 5583 for Missions). No whitescreen on any page. `playground-shim` is a redirect to Plugins (intentional; ships as a demo-tier chip until lane-playground-page lands), and Optional pages (Playground/Missions) correctly toggle visible when `hiddenPages=[]`. + +### ③ Chat page three-layer views switchable +- List/Graph tab switcher wired via `.chat-view-tab[data-chat-view-tab]`; clicking Graph flips `.pane[data-pane="chat"][data-chat-view]` to `graph` and mounts `#chat-session-graph` (SVG or empty-state text present). +- Details drawer (`#chat-side-drawer-btn`, `.chat-side-drawer`) is visible (`display:flex`, `aria-expanded="true"`) on cold boot; toggling the button flips the drawer state. + +### ④ Context page proportion bars render +`.context-band` / `.ctx-share-bar` element count on Context tab: **61** (session default renders inject/compact/recall bands even with no active session, per the "Open a session to see..." empty state). + +### ⑤ Rubrics 7 tiles have data + Runtime cell-jump to tracing +- Rubrics page: **7 tiles** rendered, all 7 have data payloads (`rubricWithData: 7`). Matches the shipped `docs/rubric-fusion-fixture.json` count. +- Cell-jump: clicking a `.rubric-grid-cell` on Rubrics page navigates to Tracing tab (`afterRubricClick: "tracing"`). Runtimes page presents 34 rubric-grid cells with the same wiring. + +### ⑥ Artifacts panel first-visit + mock button triggers Board display +- On Chat page first visit: `.artifact-panel` mounts eagerly via `ensureEmptyPanel()` before any artifact event. +- Empty-state hint (`.artifact-panel-empty[data-role="empty-state"]`) is present until first event. +- Clicking Hub → `#mock-artifact` button seeds fixtures via `seedBoardFixture(seed.artifacts)`: 6 list items + 12 artifact cards rendered, empty hint cleared. + +### ⑦ Trace signal chip +Verified by unit test — `test/trace-signal-detect.test.js` and `test/turn-signal-chip.test.js` cover chip attach path (`applyTurnSignalChips` in renderer.js:1576). In cold-boot mock-inject scenarios the loop/redundant thresholds do not fire (mocks emit single-turn events that don't cross detection thresholds); chips do render in real streaming sessions when detectSignals returns non-empty (behavior-tested in the green suite). + +### ⑧ hiddenPages three-state validated +| Config | Hidden buttons (data-tab) | Result | +|---|---|---| +| `hiddenPages: undefined` (default) | `["playground-shim", "mission"]` | PASS — DEFAULT_HIDDEN applied | +| `hiddenPages: []` | `[]` | PASS — all nav items visible | +| `hiddenPages: ["prs"]` | `["prs"]` | PASS — custom list honored | + +IPC `nav:setHiddenPages` correctly rejects non-array input; `nav-config-model.resolveHiddenPages(cfg)` returns three distinct branches (missing → default set, empty array → nothing hidden, non-empty → honored as-is). + +## 4. Cold-clone end-to-end — **PASS** + +Fresh clone of dev-clone branch to `/tmp/dsh-final-clone` (PR head `d308769d2...`; usability merge rides on top of nav in the main-repo assembly, PR includes both when this doc pushes). + +``` +$ git clone -b feat/dsh-desktop-shell ~/harness/deepseek-harness-dev /tmp/dsh-final-clone +$ cd /tmp/dsh-final-clone && pnpm install --prefer-offline --ignore-scripts +$ cd examples/desktop && npm install +$ LEFTHOOK=0 npm test +# tests 1789 +# pass 1789 +# fail 0 +$ DSH_DESKTOP_HOME=/tmp/dsh-final-clone-home node test/smoke-runtime.js stdio +=== stdio-echo === +[stdio] status=starting +[stdio] 6 notifications +[stdio] status=dead +[smoke] OK +``` + +`stdio-echo` preflight sends one prompt over the JSON-RPC wire, receives the six-event notification stream (`turn/start`, `user/message`, `step/start`, `step/end`, `turn/end`, `session.finished`), and reports `[smoke] OK`. External cloner usability confirmed. + +## 5. Conclusion — **整体可用** + +All 5 gate items passed on final HEAD `08837bd96717fcc7f63e38d7f6918d35f13bec00`. No blocking issues surfaced by the integration probe. Zero console errors, zero uncaught exceptions during cold boot + 14-page sweep + hiddenPages three-state exercise. Cold-clone external usability verified via stdio-echo preflight. diff --git a/examples/desktop/docs/rubric-fusion-fixture.json b/examples/desktop/docs/rubric-fusion-fixture.json index 77cafcc07c..18c246c099 100644 --- a/examples/desktop/docs/rubric-fusion-fixture.json +++ b/examples/desktop/docs/rubric-fusion-fixture.json @@ -2,7 +2,7 @@ "_note": "Rubric fusion fixture — feeds Rubrics/Growth/Runtime demo. All demo-tier data.", "rubrics": [ { - "id": "svg-gen", + "id": "svg-generation", "name": "SVG generation", "group": "interaction-reasoning", "description": "Iterative SVG editing over 3-5 turns. Judged for shape correctness, style consistency, and no-regression.", @@ -33,6 +33,42 @@ { "id": "fix-effectiveness", "label": "Fix effectiveness", "type": "continuous", "min": 1, "max": 5 }, { "id": "convergence", "label": "Convergence", "type": "continuous", "min": 1, "max": 5 } ] + }, + { + "id": "code-review", + "name": "Code review", + "group": "se-process", + "description": "Reviewer rubric — style, correctness, test-coverage feedback quality.", + "dims": [ + { "id": "verdict", "label": "Review verdict", "type": "categorical", "values": ["reject", "revise", "approve"] } + ] + }, + { + "id": "correctness-score", + "name": "Correctness score", + "group": "fix-optimize", + "description": "Continuous 0-1 correctness on trajectory's stated goal.", + "dims": [ + { "id": "correctness", "label": "Correctness", "type": "continuous", "min": 0, "max": 1 } + ] + }, + { + "id": "intent-triage", + "name": "Intent triage", + "group": "interaction-reasoning", + "description": "Categorical verdict — bad · ok · good.", + "dims": [ + { "id": "verdict", "label": "Triage verdict", "type": "categorical", "values": ["bad", "ok", "good"] } + ] + }, + { + "id": "passes-bench", + "name": "Passes bench", + "group": "se-process", + "description": "Boolean pass/fail on bench probe.", + "dims": [ + { "id": "passes", "label": "Passes bench probe", "type": "boolean", "labels": { "true": "pass", "false": "fail" } } + ] } ], "similarClasses": [ @@ -45,31 +81,31 @@ } ], "events": [ - { "ts": 1720915200000, "rubricId": "svg-gen", "dimId": "shape-correct", "sessionId": "s-svg-1", "turnId": "t1", "rolloutIdx": 1, "score": 0.42, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, - { "ts": 1720915200000, "rubricId": "svg-gen", "dimId": "style-consistent", "sessionId": "s-svg-1", "turnId": "t1", "rolloutIdx": 1, "score": 0.55, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, - { "ts": 1720915200000, "rubricId": "svg-gen", "dimId": "no-regress", "sessionId": "s-svg-1", "turnId": "t1", "rolloutIdx": 1, "score": true, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, - { "ts": 1721001600000, "rubricId": "svg-gen", "dimId": "shape-correct", "sessionId": "s-svg-2", "turnId": "t2", "rolloutIdx": 2, "score": 0.51, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, - { "ts": 1721001600000, "rubricId": "svg-gen", "dimId": "style-consistent", "sessionId": "s-svg-2", "turnId": "t2", "rolloutIdx": 2, "score": 0.60, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, - { "ts": 1721001600000, "rubricId": "svg-gen", "dimId": "no-regress", "sessionId": "s-svg-2", "turnId": "t2", "rolloutIdx": 2, "score": false, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, - { "ts": 1721088000000, "rubricId": "svg-gen", "dimId": "shape-correct", "sessionId": "s-svg-3", "turnId": "t3", "rolloutIdx": 3, "score": 0.72, "harnessVersion": "v0.10", "model": "deepseek-r1", "dataMix": "mix-b" }, - { "ts": 1721088000000, "rubricId": "svg-gen", "dimId": "style-consistent", "sessionId": "s-svg-3", "turnId": "t3", "rolloutIdx": 3, "score": 0.78, "harnessVersion": "v0.10", "model": "deepseek-r1", "dataMix": "mix-b" }, - { "ts": 1721088000000, "rubricId": "svg-gen", "dimId": "no-regress", "sessionId": "s-svg-3", "turnId": "t3", "rolloutIdx": 3, "score": true, "harnessVersion": "v0.10", "model": "deepseek-r1", "dataMix": "mix-b" }, - { "ts": 1721174400000, "rubricId": "svg-gen", "dimId": "shape-correct", "sessionId": "s-svg-4", "turnId": "t4", "rolloutIdx": 4, "score": 0.85, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-b" }, - { "ts": 1721174400000, "rubricId": "svg-gen", "dimId": "style-consistent", "sessionId": "s-svg-4", "turnId": "t4", "rolloutIdx": 4, "score": 0.82, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-b" }, - { "ts": 1721174400000, "rubricId": "svg-gen", "dimId": "no-regress", "sessionId": "s-svg-4", "turnId": "t4", "rolloutIdx": 4, "score": true, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-b" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "shape-correct", "sessionId": "s-svg-5", "turnId": "t5", "rolloutIdx": 5, "score": 0.91, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "style-consistent", "sessionId": "s-svg-5", "turnId": "t5", "rolloutIdx": 5, "score": 0.88, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "no-regress", "sessionId": "s-svg-5", "turnId": "t5", "rolloutIdx": 5, "score": true, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1720915200000, "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-1", "turnId": "t1", "rolloutIdx": 1, "score": 0.42, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, + { "ts": 1720915200000, "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-1", "turnId": "t1", "rolloutIdx": 1, "score": 0.55, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, + { "ts": 1720915200000, "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-1", "turnId": "t1", "rolloutIdx": 1, "score": true, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, + { "ts": 1721001600000, "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-2", "turnId": "t2", "rolloutIdx": 2, "score": 0.51, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, + { "ts": 1721001600000, "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-2", "turnId": "t2", "rolloutIdx": 2, "score": 0.60, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, + { "ts": 1721001600000, "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-2", "turnId": "t2", "rolloutIdx": 2, "score": false, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, + { "ts": 1721088000000, "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-3", "turnId": "t3", "rolloutIdx": 3, "score": 0.72, "harnessVersion": "v0.10", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721088000000, "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-3", "turnId": "t3", "rolloutIdx": 3, "score": 0.78, "harnessVersion": "v0.10", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721088000000, "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-3", "turnId": "t3", "rolloutIdx": 3, "score": true, "harnessVersion": "v0.10", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721174400000, "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-4", "turnId": "t4", "rolloutIdx": 4, "score": 0.85, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721174400000, "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-4", "turnId": "t4", "rolloutIdx": 4, "score": 0.82, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721174400000, "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-4", "turnId": "t4", "rolloutIdx": 4, "score": true, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-5", "turnId": "t5", "rolloutIdx": 5, "score": 0.91, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-5", "turnId": "t5", "rolloutIdx": 5, "score": 0.88, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-5", "turnId": "t5", "rolloutIdx": 5, "score": true, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "shape-correct", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 6, "score": 0.63, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "style-consistent", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 6, "score": 0.71, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "no-regress", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 6, "score": false, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "shape-correct", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 7, "score": 0.80, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "style-consistent", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 7, "score": 0.83, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "no-regress", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 7, "score": true, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "shape-correct", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 8, "score": 0.88, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "style-consistent", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 8, "score": 0.85, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "svg-gen", "dimId": "no-regress", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 8, "score": true, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 6, "score": 0.63, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 6, "score": 0.71, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 6, "score": false, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 7, "score": 0.80, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 7, "score": 0.83, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 7, "score": true, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 8, "score": 0.88, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 8, "score": 0.85, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + { "ts": 1721260800000, "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-live", "turnId": "tL", "rolloutIdx": 8, "score": true, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, { "ts": 1720915200000, "rubricId": "bug-fix", "dimId": "reproduces", "sessionId": "s-bug-1", "turnId": "t1", "rolloutIdx": 1, "score": false, "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, { "ts": 1720915200000, "rubricId": "bug-fix", "dimId": "minimality", "sessionId": "s-bug-1", "turnId": "t1", "rolloutIdx": 1, "score": "heavy", "harnessVersion": "v0.9", "model": "deepseek-r1", "dataMix": "mix-a" }, @@ -89,6 +125,22 @@ { "ts": 1721174400000, "rubricId": "multi-turn-feedback", "dimId": "convergence", "sessionId": "s-mt-2", "turnId": "t2", "rolloutIdx": 1, "score": 4, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-b" }, { "ts": 1721260800000, "rubricId": "multi-turn-feedback", "dimId": "feedback-understanding", "sessionId": "s-mt-3", "turnId": "t3", "rolloutIdx": 1, "score": 5, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, { "ts": 1721260800000, "rubricId": "multi-turn-feedback", "dimId": "fix-effectiveness", "sessionId": "s-mt-3", "turnId": "t3", "rolloutIdx": 1, "score": 5, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, - { "ts": 1721260800000, "rubricId": "multi-turn-feedback", "dimId": "convergence", "sessionId": "s-mt-3", "turnId": "t3", "rolloutIdx": 1, "score": 5, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" } + { "ts": 1721260800000, "rubricId": "multi-turn-feedback", "dimId": "convergence", "sessionId": "s-mt-3", "turnId": "t3", "rolloutIdx": 1, "score": 5, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + + { "ts": 1721088000000, "rubricId": "code-review", "dimId": "verdict", "sessionId": "s-cr-1", "turnId": "t1", "rolloutIdx": 1, "score": "revise", "harnessVersion": "v0.10", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721174400000, "rubricId": "code-review", "dimId": "verdict", "sessionId": "s-cr-2", "turnId": "t2", "rolloutIdx": 1, "score": "approve", "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721260800000, "rubricId": "code-review", "dimId": "verdict", "sessionId": "s-cr-3", "turnId": "t3", "rolloutIdx": 1, "score": "approve", "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + + { "ts": 1721088000000, "rubricId": "correctness-score", "dimId": "correctness", "sessionId": "s-cs-1", "turnId": "t1", "rolloutIdx": 1, "score": 0.62, "harnessVersion": "v0.10", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721174400000, "rubricId": "correctness-score", "dimId": "correctness", "sessionId": "s-cs-2", "turnId": "t2", "rolloutIdx": 1, "score": 0.78, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721260800000, "rubricId": "correctness-score", "dimId": "correctness", "sessionId": "s-cs-3", "turnId": "t3", "rolloutIdx": 1, "score": 0.85, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + + { "ts": 1721088000000, "rubricId": "intent-triage", "dimId": "verdict", "sessionId": "s-it-1", "turnId": "t1", "rolloutIdx": 1, "score": "ok", "harnessVersion": "v0.10", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721174400000, "rubricId": "intent-triage", "dimId": "verdict", "sessionId": "s-it-2", "turnId": "t2", "rolloutIdx": 1, "score": "good", "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721260800000, "rubricId": "intent-triage", "dimId": "verdict", "sessionId": "s-it-3", "turnId": "t3", "rolloutIdx": 1, "score": "good", "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" }, + + { "ts": 1721088000000, "rubricId": "passes-bench", "dimId": "passes", "sessionId": "s-pb-1", "turnId": "t1", "rolloutIdx": 1, "score": false, "harnessVersion": "v0.10", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721174400000, "rubricId": "passes-bench", "dimId": "passes", "sessionId": "s-pb-2", "turnId": "t2", "rolloutIdx": 1, "score": true, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-b" }, + { "ts": 1721260800000, "rubricId": "passes-bench", "dimId": "passes", "sessionId": "s-pb-3", "turnId": "t3", "rolloutIdx": 1, "score": true, "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" } ] } diff --git a/examples/desktop/scripts/regen-artifact-board-seed.js b/examples/desktop/scripts/regen-artifact-board-seed.js new file mode 100644 index 0000000000..7a565b446c --- /dev/null +++ b/examples/desktop/scripts/regen-artifact-board-seed.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +// Regenerates src/renderer/artifact-board-seed.js from +// docs/artifact-board-fixture.json. Renderer runs at file:// so we inline +// the fixture instead of fetch(). +// +// Usage: node scripts/regen-artifact-board-seed.js + +'use strict' +const fs = require('fs') +const path = require('path') + +const root = path.resolve(__dirname, '..') +const src = path.join(root, 'docs', 'artifact-board-fixture.json') +const dst = path.join(root, 'src', 'renderer', 'artifact-board-seed.js') + +const j = JSON.parse(fs.readFileSync(src, 'utf8')) +const body = "// Auto-inlined artifact-board fixture. Renderer runs at file:// so we\n" + + "// inline docs/artifact-board-fixture.json here instead of relying on fetch().\n" + + "// To refresh: node scripts/regen-artifact-board-seed.js\n" + + "\n" + + "'use strict'\n" + + ";(function () {\n" + + " if (typeof window === 'undefined') return\n" + + " window.__dshArtifactBoardSeed = " + JSON.stringify(j, null, 2) + ";\n" + + "})()\n" +fs.writeFileSync(dst, body) +console.log('wrote', dst, '(' + body.length + ' bytes)') diff --git a/examples/desktop/scripts/regen-rubric-fusion-seed.js b/examples/desktop/scripts/regen-rubric-fusion-seed.js new file mode 100644 index 0000000000..8014bb3e34 --- /dev/null +++ b/examples/desktop/scripts/regen-rubric-fusion-seed.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +// Regenerates src/renderer/rubric-fusion-seed.js from +// docs/rubric-fusion-fixture.json. Renderer runs at file:// so we inline +// the fixture instead of fetch(). +// +// Usage: node scripts/regen-rubric-fusion-seed.js + +'use strict' +const fs = require('fs') +const path = require('path') + +const root = path.resolve(__dirname, '..') +const src = path.join(root, 'docs', 'rubric-fusion-fixture.json') +const dst = path.join(root, 'src', 'renderer', 'rubric-fusion-seed.js') + +const j = JSON.parse(fs.readFileSync(src, 'utf8')) +const body = "// Auto-inlined fusion fixture. Renderer runs at file:// so we inline\n" + + "// docs/rubric-fusion-fixture.json here instead of relying on fetch().\n" + + "// To refresh: node scripts/regen-rubric-fusion-seed.js\n" + + "\n" + + "'use strict'\n" + + ";(function () {\n" + + " if (typeof window === 'undefined') return\n" + + " window.__dshRubricFusionSeed = " + JSON.stringify(j, null, 2) + ";\n" + + "})()\n" +fs.writeFileSync(dst, body) +console.log('wrote', dst, '(' + body.length + ' bytes)') diff --git a/examples/desktop/src/renderer/artifact-board-seed.js b/examples/desktop/src/renderer/artifact-board-seed.js new file mode 100644 index 0000000000..5053bce706 --- /dev/null +++ b/examples/desktop/src/renderer/artifact-board-seed.js @@ -0,0 +1,93 @@ +// Auto-inlined artifact-board fixture. Renderer runs at file:// so we +// inline docs/artifact-board-fixture.json here instead of relying on fetch(). +// To refresh: node scripts/regen-artifact-board-seed.js + +'use strict' +;(function () { + if (typeof window === 'undefined') return + window.__dshArtifactBoardSeed = { + "_note": "Fixture for lane-artifact-v2 (Artifacts evolution chain + Board view). Each entry is one artifact:event payload the renderer would receive. Multi-version entries share an artifactId; blob content is included so the evolution diff pane can render real per-hop line-diffs. The real ArtifactServer does not include content in its broadcast — that's the constraint the strip's fallback 'content not preserved' note is calibrated for.", + "artifacts": [ + { + "artifactId": "session.md", + "kind": "md", + "version": 1, + "seenAt": 1747000000000, + "path": "/w/.artifacts/session.md", + "blob": "# Session log\n\nTurn 1: user asked to build a landing page.\n" + }, + { + "artifactId": "session.md", + "kind": "md", + "version": 2, + "seenAt": 1747000060000, + "path": "/w/.artifacts/session.md", + "blob": "# Session log\n\nTurn 1: user asked to build a landing page.\nTurn 2: agent scaffolded landing.html and hero.svg.\n" + }, + { + "artifactId": "session.md", + "kind": "md", + "version": 3, + "seenAt": 1747000180000, + "path": "/w/.artifacts/session.md", + "blob": "# Session log\n\nTurn 1: user asked to build a landing page.\nTurn 2: agent scaffolded landing.html and hero.svg.\nTurn 3: agent added responsive styles and OG tags.\nTurn 4: agent added README with build steps.\n" + }, + { + "artifactId": "landing.html", + "kind": "html", + "version": 1, + "seenAt": 1747000030000, + "path": "/w/.artifacts/landing.html", + "blob": "\n\n
Welcome to our product.
\n\n\n" + }, + { + "artifactId": "hero.svg", + "kind": "svg", + "version": 1, + "seenAt": 1747000045000, + "path": "/w/.artifacts/hero.svg", + "blob": "\n" + }, + { + "artifactId": "config.json", + "kind": "json", + "version": 1, + "seenAt": 1747000120000, + "path": "/w/.artifacts/config.json", + "blob": "{\n \"name\": \"landing\",\n \"version\": \"0.1.0\"\n}\n" + }, + { + "artifactId": "config.json", + "kind": "json", + "version": 2, + "seenAt": 1747000200000, + "path": "/w/.artifacts/config.json", + "blob": "{\n \"name\": \"landing\",\n \"version\": \"0.2.0\",\n \"port\": 3000\n}\n" + }, + { + "artifactId": "build.sh", + "kind": "sh", + "version": 1, + "seenAt": 1747000150000, + "path": "/w/.artifacts/build.sh", + "blob": "#!/bin/sh\nset -e\nnpm install\nnpm run build\n" + }, + { + "artifactId": "README.md", + "kind": "md", + "version": 1, + "seenAt": 1747000210000, + "path": "/w/.artifacts/README.md", + "blob": "# Landing\n\nStatic landing page for the demo product.\n\n## Build\n\n```sh\nsh build.sh\n```\n" + } + ] +}; +})() diff --git a/examples/desktop/src/renderer/artifacts-board.js b/examples/desktop/src/renderer/artifacts-board.js index c240799013..fa6fc04921 100644 --- a/examples/desktop/src/renderer/artifacts-board.js +++ b/examples/desktop/src/renderer/artifacts-board.js @@ -292,6 +292,20 @@ head.append(label, summary) el.append(head) + // Provenance banner: the real ArtifactServer broadcasts fs writes + // with no blob content, so per-hop diffs collapse to the "content + // not preserved" fallback in production. When any hop is missing + // blob content we surface why up front rather than letting the + // researcher assume the diff pane is broken. Fixture supplies + // blobs so all hops render; production doesn't, yet. + const missingBlobs = chain.some((c) => typeof c.blob !== 'string') + if (missingBlobs) { + const banner = document.createElement('div') + banner.className = 'artifact-evolution-banner muted small' + banner.textContent = 'Version diff currently requires blob payload. The real ArtifactServer does not preserve older blobs (RFC L-3 follow-up: snapshot store). The fixture supplies blobs for demo.' + el.append(banner) + } + const chainEl = document.createElement('ol') chainEl.className = 'artifact-evolution-chain' for (let i = 0; i < chain.length; i++) { diff --git a/examples/desktop/src/renderer/artifacts.js b/examples/desktop/src/renderer/artifacts.js index 9a6c33d70b..172c7be4d2 100644 --- a/examples/desktop/src/renderer/artifacts.js +++ b/examples/desktop/src/renderer/artifacts.js @@ -176,6 +176,12 @@ b.panelEl = buildPanel() stream.appendChild(b.panelEl) } + // Remove the cold-clone empty-state hint (if present) once we have + // a real artifact to show. Introduced by lane-usability-fix so + // seedBoardFixture's placeholder gets swept when a real artifact + // event arrives. + const emptyHint = b.panelEl.querySelector('[data-role="empty-state"]') + if (emptyHint && emptyHint.parentNode) emptyHint.parentNode.removeChild(emptyHint) const groupHost = b.panelEl.querySelector('.artifact-group') groupHost.appendChild(el) } @@ -458,32 +464,87 @@ setTimeout(() => el.classList.remove('artifact-flash'), 900) } - // Debug menu button — mocks a write into the artifact dir via IPC. + // Debug menu button — seeds the board/timeline/evolution demo. The + // real ArtifactServer broadcasts fs writes with no blob content, so + // cold-clone researchers never see the Board/Timeline/Evolution + // views' actual affordances. We route the multi-kind, multi-version + // fixture through the same onArtifactEvent entry the real wire uses + // so all three tabs light up via the production code path. When the + // seed is unavailable we fall back to the original IPC single-file + // mock. function bindMockButton() { const btn = document.getElementById('mock-artifact') if (!btn) return btn.addEventListener('click', async () => { btn.disabled = true try { - await window.dsh.mockArtifact() + const seed = typeof window !== 'undefined' ? window.__dshArtifactBoardSeed : null + if (seed && Array.isArray(seed.artifacts) && seed.artifacts.length) { + seedBoardFixture(seed.artifacts) + } else if (window.dsh && typeof window.dsh.mockArtifact === 'function') { + await window.dsh.mockArtifact() + } } catch (err) { - console.error('mockArtifact failed', err) + console.error('mockArtifact / seed failed', err) } finally { btn.disabled = false } }) } + // Feed the board fixture through the same onArtifactEvent path the + // real ArtifactServer uses. `seenAt` is re-anchored to "moments ago" + // so relative-time labels don't read as ~430d against the frozen + // fixture timestamps. Idempotent: ensureCard() de-dups on artifactId, + // recordHistory() de-dups on (artifactId, version). + function seedBoardFixture(list) { + if (!Array.isArray(list) || !list.length) return + const base = Date.now() - list.length * 60_000 + list.forEach((raw, i) => { + onArtifactEvent({ ...raw, seenAt: base + i * 60_000 }) + }) + } + + // Cold-clone empty state: mount the panel scaffold on stream init so + // researchers can see List/Board/Timeline tabs (and the "no artifacts + // yet" hint) exist even before the first artifact:event lands. Until + // now the panel only appeared after the first artifact — new users + // saw a blank pane and had no idea the surface existed. Scoped to the + // active session bucket so switching sessions before any artifact + // fires still gets its own empty panel per session. + function ensureEmptyPanel() { + const s = streamEl() + if (!s) return + const b = bucket() + if (b.panelEl && b.panelEl.isConnected) return + b.panelEl = buildPanel() + // Inject a muted empty-state hint into the List body. Removed on + // first appendGrouped() call so real artifacts land in a clean host. + const body = b.panelEl.querySelector('.artifact-panel-body') + if (body) { + const hint = document.createElement('div') + hint.className = 'artifact-panel-empty muted small' + hint.dataset.role = 'empty-state' + hint.textContent = 'No artifacts yet — try “artifact” in the Debug menu, or run a session that writes to .artifacts/.' + body.appendChild(hint) + } + s.appendChild(b.panelEl) + } + // Wire up once the DOM + preload bridge are ready. The renderer script // tag is loaded after this one, so we just register the listener // eagerly. if (window.dsh && typeof window.dsh.onArtifact === 'function') { window.dsh.onArtifact(onArtifactEvent) } - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', bindMockButton) - } else { + function initDom() { bindMockButton() + ensureEmptyPanel() + } + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initDom) + } else { + initDom() } // Route artifact events on a session id if the payload carries one. @@ -510,6 +571,8 @@ cards, history, switchView, + seedBoardFixture, + ensureEmptyPanel, getView: () => bucket().currentView, setActiveSession, getActiveSessionId: () => activeSessionId, diff --git a/examples/desktop/src/renderer/chat-session-graph.js b/examples/desktop/src/renderer/chat-session-graph.js index a1bae51aa1..08f25718c8 100644 --- a/examples/desktop/src/renderer/chat-session-graph.js +++ b/examples/desktop/src/renderer/chat-session-graph.js @@ -169,9 +169,20 @@ function renderSessionGraph(container, snapshot) { label.setAttribute('y', String(pos.y + 4)) label.textContent = node.label g.appendChild(label) - if (typeof snapshot?.onSelect === 'function' && node.turnId) { + // Bind click on any node that carries an actionable identifier: + // turn nodes → turnId, fork nodes → childSessionId, user nodes → + // seq. Only these get the pointer cursor; nodes without a target + // fall back to the default cursor so the affordance matches + // reality. + const actionable = + (node.turnId) || + (node.kind === 'fork' && node.childSessionId) || + (node.kind === 'user' && node.seq) + if (typeof snapshot?.onSelect === 'function' && actionable) { g.addEventListener('click', () => snapshot.onSelect(node)) g.style && (g.style.cursor = 'pointer') + } else if (g.style) { + g.style.cursor = 'default' } svg.appendChild(g) } diff --git a/examples/desktop/src/renderer/index.html b/examples/desktop/src/renderer/index.html index 381845d2cf..3f5a3ba062 100644 --- a/examples/desktop/src/renderer/index.html +++ b/examples/desktop/src/renderer/index.html @@ -1369,6 +1369,11 @@ Evolution renderers) and MUST load before artifacts.js so the L0 row version-chip click handler + view switcher find the module ready. --> + + diff --git a/examples/desktop/src/renderer/renderer.js b/examples/desktop/src/renderer/renderer.js index 873a1e2d8a..127e660f72 100644 --- a/examples/desktop/src/renderer/renderer.js +++ b/examples/desktop/src/renderer/renderer.js @@ -4291,13 +4291,29 @@ function refreshChatSideDrawer() { sessionId: state.activeSessionId || '', model: meta && (meta.model || (meta.header && meta.header.model)) || '', events, - selectedTurnId: null, + selectedTurnId: state.chatSelectedTurnId || null, onSelect(row) { - if (!row || !row.turnId) return + if (!row) return + // User rows have no turnId — scroll to the matching data-seq + // anchor instead of the no-op early return. Cursor:pointer on + // the whole row promises interactivity; without this the row + // reads as a dead affordance. + if (row.kind === 'user' && row.seq) { + const seqTarget = streamEl && streamEl.querySelector(`[data-seq="${row.seq}"]`) + if (seqTarget && typeof seqTarget.scrollIntoView === 'function') { + seqTarget.scrollIntoView({ behavior: 'smooth', block: 'center' }) + } + return + } + if (!row.turnId) return + state.chatSelectedTurnId = row.turnId const target = streamEl && streamEl.querySelector(`[data-turn-id="${row.turnId}"]`) if (target && typeof target.scrollIntoView === 'function') { target.scrollIntoView({ behavior: 'smooth', block: 'center' }) } + // Re-render so the .active highlight lands on the row we just + // clicked. Keeps drawer state in sync with the stream focus. + refreshChatSideDrawerIfOpen() }, }) } @@ -4343,15 +4359,34 @@ function refreshSessionGraph() { const events = (meta && Array.isArray(meta.cachedEvents)) ? meta.cachedEvents : [] api.renderSessionGraph(chatSessionGraphEl, { sessionId: state.activeSessionId || '', + selectedTurnId: state.chatSelectedTurnId || null, events, onSelect(node) { - if (!node || !node.turnId) return + if (!node) return + // Fork nodes point at a child session — clicking follows the + // fork. Without this the cursor:pointer node reads as dead. + if (node.kind === 'fork' && node.childSessionId) { + try { selectSession(node.childSessionId) } catch (_) { /* stale id */ } + return + } + // User nodes have no turnId — scroll to the message via data-seq. + if (node.kind === 'user' && node.seq) { + setChatView('list') + const sTarget = streamEl && streamEl.querySelector(`[data-seq="${node.seq}"]`) + if (sTarget && typeof sTarget.scrollIntoView === 'function') { + sTarget.scrollIntoView({ behavior: 'smooth', block: 'center' }) + } + return + } + if (!node.turnId) return + state.chatSelectedTurnId = node.turnId // Jump to the turn in the List view and focus it. setChatView('list') const target = streamEl && streamEl.querySelector(`[data-turn-id="${node.turnId}"]`) if (target && typeof target.scrollIntoView === 'function') { target.scrollIntoView({ behavior: 'smooth', block: 'center' }) } + refreshChatSideDrawerIfOpen() }, }) } diff --git a/examples/desktop/src/renderer/rubric-fusion-model.js b/examples/desktop/src/renderer/rubric-fusion-model.js index 42c4b994e9..907791e394 100644 --- a/examples/desktop/src/renderer/rubric-fusion-model.js +++ b/examples/desktop/src/renderer/rubric-fusion-model.js @@ -178,13 +178,16 @@ function createStore() { // so without dedupe here, two-page seeding would double-insert every // event. Two-tier guard: // 1. WeakSet on the fixture object ref (fast path for the common - // case where all pages read window.__dshRubricFusionSeed). + // case where all pages read window.__dshRubricFusionSeed). Return + // shape includes `deduped: true` on this fast-path hit so callers + // can distinguish "already seeded" from "empty fixture". // 2. Per-event key dedupe inside addEvent() catches the case where a - // caller passes a fresh literal that happens to carry the same - // rows. + // caller passes a fresh literal (or a deep-cloned copy) that + // carries the same rows — the WeakSet won't help there because + // the object identity differs. function loadFixture(json) { if (!json || typeof json !== 'object') return { rubrics: 0, events: 0 } - if (state.loadedFixtures.has(json)) return { rubrics: 0, events: 0 } + if (state.loadedFixtures.has(json)) return { rubrics: 0, events: 0, deduped: true } state.loadedFixtures.add(json) let rn = 0, en = 0 for (const r of json.rubrics || []) { if (registerRubric(r)) rn++ } diff --git a/examples/desktop/src/renderer/rubric-fusion-seed.js b/examples/desktop/src/renderer/rubric-fusion-seed.js index 66747304b2..d937c5238a 100644 --- a/examples/desktop/src/renderer/rubric-fusion-seed.js +++ b/examples/desktop/src/renderer/rubric-fusion-seed.js @@ -1,3 +1,7 @@ +// Auto-inlined fusion fixture. Renderer runs at file:// so we inline +// docs/rubric-fusion-fixture.json here instead of relying on fetch(). +// To refresh: node scripts/regen-rubric-fusion-seed.js + 'use strict' ;(function () { if (typeof window === 'undefined') return @@ -5,7 +9,7 @@ "_note": "Rubric fusion fixture — feeds Rubrics/Growth/Runtime demo. All demo-tier data.", "rubrics": [ { - "id": "svg-gen", + "id": "svg-generation", "name": "SVG generation", "group": "interaction-reasoning", "description": "Iterative SVG editing over 3-5 turns. Judged for shape correctness, style consistency, and no-regression.", @@ -97,6 +101,74 @@ "max": 5 } ] + }, + { + "id": "code-review", + "name": "Code review", + "group": "se-process", + "description": "Reviewer rubric — style, correctness, test-coverage feedback quality.", + "dims": [ + { + "id": "verdict", + "label": "Review verdict", + "type": "categorical", + "values": [ + "reject", + "revise", + "approve" + ] + } + ] + }, + { + "id": "correctness-score", + "name": "Correctness score", + "group": "fix-optimize", + "description": "Continuous 0-1 correctness on trajectory's stated goal.", + "dims": [ + { + "id": "correctness", + "label": "Correctness", + "type": "continuous", + "min": 0, + "max": 1 + } + ] + }, + { + "id": "intent-triage", + "name": "Intent triage", + "group": "interaction-reasoning", + "description": "Categorical verdict — bad · ok · good.", + "dims": [ + { + "id": "verdict", + "label": "Triage verdict", + "type": "categorical", + "values": [ + "bad", + "ok", + "good" + ] + } + ] + }, + { + "id": "passes-bench", + "name": "Passes bench", + "group": "se-process", + "description": "Boolean pass/fail on bench probe.", + "dims": [ + { + "id": "passes", + "label": "Passes bench probe", + "type": "boolean", + "labels": { + "true": "pass", + "false": "fail" + } + } + ] } ], "similarClasses": [ @@ -117,7 +189,7 @@ "events": [ { "ts": 1720915200000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-1", "turnId": "t1", @@ -129,7 +201,7 @@ }, { "ts": 1720915200000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-1", "turnId": "t1", @@ -141,7 +213,7 @@ }, { "ts": 1720915200000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-1", "turnId": "t1", @@ -153,7 +225,7 @@ }, { "ts": 1721001600000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-2", "turnId": "t2", @@ -165,7 +237,7 @@ }, { "ts": 1721001600000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-2", "turnId": "t2", @@ -177,7 +249,7 @@ }, { "ts": 1721001600000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-2", "turnId": "t2", @@ -189,7 +261,7 @@ }, { "ts": 1721088000000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-3", "turnId": "t3", @@ -201,7 +273,7 @@ }, { "ts": 1721088000000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-3", "turnId": "t3", @@ -213,7 +285,7 @@ }, { "ts": 1721088000000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-3", "turnId": "t3", @@ -225,7 +297,7 @@ }, { "ts": 1721174400000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-4", "turnId": "t4", @@ -237,7 +309,7 @@ }, { "ts": 1721174400000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-4", "turnId": "t4", @@ -249,7 +321,7 @@ }, { "ts": 1721174400000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-4", "turnId": "t4", @@ -261,7 +333,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-5", "turnId": "t5", @@ -273,7 +345,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-5", "turnId": "t5", @@ -285,7 +357,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-5", "turnId": "t5", @@ -297,7 +369,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-live", "turnId": "tL", @@ -309,7 +381,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-live", "turnId": "tL", @@ -321,7 +393,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-live", "turnId": "tL", @@ -333,7 +405,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-live", "turnId": "tL", @@ -345,7 +417,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-live", "turnId": "tL", @@ -357,7 +429,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-live", "turnId": "tL", @@ -369,7 +441,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "shape-correct", "sessionId": "s-svg-live", "turnId": "tL", @@ -381,7 +453,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "style-consistent", "sessionId": "s-svg-live", "turnId": "tL", @@ -393,7 +465,7 @@ }, { "ts": 1721260800000, - "rubricId": "svg-gen", + "rubricId": "svg-generation", "dimId": "no-regress", "sessionId": "s-svg-live", "turnId": "tL", @@ -618,6 +690,150 @@ "harnessVersion": "v0.11", "model": "deepseek-r1", "dataMix": "mix-c" + }, + { + "ts": 1721088000000, + "rubricId": "code-review", + "dimId": "verdict", + "sessionId": "s-cr-1", + "turnId": "t1", + "rolloutIdx": 1, + "score": "revise", + "harnessVersion": "v0.10", + "model": "deepseek-r1", + "dataMix": "mix-b" + }, + { + "ts": 1721174400000, + "rubricId": "code-review", + "dimId": "verdict", + "sessionId": "s-cr-2", + "turnId": "t2", + "rolloutIdx": 1, + "score": "approve", + "harnessVersion": "v0.11", + "model": "deepseek-r1", + "dataMix": "mix-b" + }, + { + "ts": 1721260800000, + "rubricId": "code-review", + "dimId": "verdict", + "sessionId": "s-cr-3", + "turnId": "t3", + "rolloutIdx": 1, + "score": "approve", + "harnessVersion": "v0.11", + "model": "deepseek-r1", + "dataMix": "mix-c" + }, + { + "ts": 1721088000000, + "rubricId": "correctness-score", + "dimId": "correctness", + "sessionId": "s-cs-1", + "turnId": "t1", + "rolloutIdx": 1, + "score": 0.62, + "harnessVersion": "v0.10", + "model": "deepseek-r1", + "dataMix": "mix-b" + }, + { + "ts": 1721174400000, + "rubricId": "correctness-score", + "dimId": "correctness", + "sessionId": "s-cs-2", + "turnId": "t2", + "rolloutIdx": 1, + "score": 0.78, + "harnessVersion": "v0.11", + "model": "deepseek-r1", + "dataMix": "mix-b" + }, + { + "ts": 1721260800000, + "rubricId": "correctness-score", + "dimId": "correctness", + "sessionId": "s-cs-3", + "turnId": "t3", + "rolloutIdx": 1, + "score": 0.85, + "harnessVersion": "v0.11", + "model": "deepseek-r1", + "dataMix": "mix-c" + }, + { + "ts": 1721088000000, + "rubricId": "intent-triage", + "dimId": "verdict", + "sessionId": "s-it-1", + "turnId": "t1", + "rolloutIdx": 1, + "score": "ok", + "harnessVersion": "v0.10", + "model": "deepseek-r1", + "dataMix": "mix-b" + }, + { + "ts": 1721174400000, + "rubricId": "intent-triage", + "dimId": "verdict", + "sessionId": "s-it-2", + "turnId": "t2", + "rolloutIdx": 1, + "score": "good", + "harnessVersion": "v0.11", + "model": "deepseek-r1", + "dataMix": "mix-b" + }, + { + "ts": 1721260800000, + "rubricId": "intent-triage", + "dimId": "verdict", + "sessionId": "s-it-3", + "turnId": "t3", + "rolloutIdx": 1, + "score": "good", + "harnessVersion": "v0.11", + "model": "deepseek-r1", + "dataMix": "mix-c" + }, + { + "ts": 1721088000000, + "rubricId": "passes-bench", + "dimId": "passes", + "sessionId": "s-pb-1", + "turnId": "t1", + "rolloutIdx": 1, + "score": false, + "harnessVersion": "v0.10", + "model": "deepseek-r1", + "dataMix": "mix-b" + }, + { + "ts": 1721174400000, + "rubricId": "passes-bench", + "dimId": "passes", + "sessionId": "s-pb-2", + "turnId": "t2", + "rolloutIdx": 1, + "score": true, + "harnessVersion": "v0.11", + "model": "deepseek-r1", + "dataMix": "mix-b" + }, + { + "ts": 1721260800000, + "rubricId": "passes-bench", + "dimId": "passes", + "sessionId": "s-pb-3", + "turnId": "t3", + "rolloutIdx": 1, + "score": true, + "harnessVersion": "v0.11", + "model": "deepseek-r1", + "dataMix": "mix-c" } ] }; diff --git a/examples/desktop/src/renderer/style.css b/examples/desktop/src/renderer/style.css index e30fe6a6c2..2ecd7354df 100644 --- a/examples/desktop/src/renderer/style.css +++ b/examples/desktop/src/renderer/style.css @@ -12574,6 +12574,18 @@ button.artifact-version:hover { border: 1px dashed var(--border); border-radius: 4px; margin-top: 4px; } +.artifact-evolution-banner { + padding: 6px 8px; font-size: 11px; line-height: 1.4; + border-left: 2px solid var(--border); + background: var(--bg-elev); + border-radius: 0 4px 4px 0; + margin: 4px 0 6px 0; +} +.artifact-panel-empty { + padding: 12px 10px; + text-align: center; + line-height: 1.5; +} .artifact-evolution-diff-body { margin-top: 4px; border: 1px solid var(--border); border-radius: 4px; diff --git a/examples/desktop/src/renderer/tracing-page.js b/examples/desktop/src/renderer/tracing-page.js index 7223414cf4..e07edf8bd2 100644 --- a/examples/desktop/src/renderer/tracing-page.js +++ b/examples/desktop/src/renderer/tracing-page.js @@ -472,4 +472,17 @@ // tri-view instead of on the index. Auto-drill only fires when the caller // has a valid sessionId; the internal path already handles refresh + rehydrate. window.__dshTracingPage = { show, closeDrill, openDrill } + + // Runtime → Rubric grid cell click dispatches `dsh:rubric-cell-jump`. The + // upstream fires with { sessionId, turnId } detail and calls __dshTabs to + // switch, but drops the ids. Wire the drill here so clicking a cell + // actually lands the user inside that session's trace — the whole value + // prop of the grid. + if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') { + window.addEventListener('dsh:rubric-cell-jump', function (ev) { + const detail = ev && ev.detail + if (!detail || !detail.sessionId) return + try { openDrill(detail.sessionId) } catch (_) { /* drill unavailable — swallow */ } + }) + } })() diff --git a/examples/desktop/test/rubric-fusion-views.test.js b/examples/desktop/test/rubric-fusion-views.test.js index fe8b1694ee..a0f2181479 100644 --- a/examples/desktop/test/rubric-fusion-views.test.js +++ b/examples/desktop/test/rubric-fusion-views.test.js @@ -29,10 +29,14 @@ function freshFusion() { const FIXTURE_PATH = path.join(__dirname, '..', 'docs', 'rubric-fusion-fixture.json') const FIXTURE = JSON.parse(fs.readFileSync(FIXTURE_PATH, 'utf8')) -test('fixture loads with 3 rubrics and > 30 events', () => { +test('fixture loads with catalog-aligned rubrics and > 30 events', () => { const s = freshFusion() const res = s.loadFixture(FIXTURE) - assert.equal(res.rubrics, 3) + // Fixture now covers all 7 catalog rubrics (svg-generation, bug-fix, + // multi-turn-feedback, code-review, correctness-score, intent-triage, + // passes-bench) so every Rubrics tile has stats. Before the fix only + // 3 rubrics were seeded and 4 tiles read as "No scores yet." + assert.equal(res.rubrics, 7) assert.ok(res.events > 30, 'expected > 30 events, got ' + res.events) }) @@ -92,8 +96,8 @@ test('Growth view: filter chip harnessVersion narrows the series', () => { test('Runtime view: rolloutGridFor produces a matrix with cell pass/fail', () => { const s = freshFusion() s.loadFixture(FIXTURE) - const grid = s.rolloutGridFor('svg-gen', 's-svg-live') - assert.equal(grid.rubric.id, 'svg-gen') + const grid = s.rolloutGridFor('svg-generation', 's-svg-live') + assert.equal(grid.rubric.id, 'svg-generation') assert.ok(grid.rollouts.length >= 3, 'expected >= 3 rollouts for the live session') assert.ok(grid.dims.length === 3, 'svg-gen has 3 dims') // Assert we have at least one pass AND at least one fail — the fixture @@ -107,7 +111,7 @@ test('Runtime view: rolloutGridFor produces a matrix with cell pass/fail', () => test('Runtime view: rolloutGridFor sessionId=null aggregates all rollouts', () => { const s = freshFusion() s.loadFixture(FIXTURE) - const grid = s.rolloutGridFor('svg-gen', null) + const grid = s.rolloutGridFor('svg-generation', null) // The fixture has rollouts 1..8 for svg-gen (5 seed sessions + 3 on // the live session). assert.deepEqual(grid.rollouts, [1, 2, 3, 4, 5, 6, 7, 8]) diff --git a/examples/desktop/test/usability-p0-batch.test.js b/examples/desktop/test/usability-p0-batch.test.js new file mode 100644 index 0000000000..1345d8a106 --- /dev/null +++ b/examples/desktop/test/usability-p0-batch.test.js @@ -0,0 +1,100 @@ +// Test locks for the fix/usability-p0-batch cycle. Each locks one of the +// P0 findings from review-usability: fixture id/catalog match, fusion +// loadFixture idempotency, rubric-cell-jump listener presence, artifact +// blob banner on missing-blob chains, artifact seed API surface. + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') +const fs = require('fs') +const path = require('path') + +const RENDERER = path.resolve(__dirname, '..', 'src', 'renderer') + +// -- P0-3 fusion loadFixture is idempotent --------------------------------- +test('rubric-fusion loadFixture is idempotent by fixture identity', () => { + const fusion = require(path.join(RENDERER, 'rubric-fusion-model.js')) + const store = fusion.create() + const fixture = { + rubrics: [{ id: 'r1', name: 'R1', dims: [{ id: 'd1', label: 'D1', type: 'continuous', min: 0, max: 1 }] }], + events: [{ ts: 1, rubricId: 'r1', dimId: 'd1', sessionId: 's', turnId: 't', score: 0.5 }], + } + const first = store.loadFixture(fixture) + const second = store.loadFixture(fixture) + const third = store.loadFixture(fixture) + assert.strictEqual(first.events, 1, 'first load registers events') + assert.strictEqual(second.events, 0, 'second load is a no-op') + assert.ok(second.deduped, 'second load reports deduped flag') + assert.strictEqual(third.events, 0, 'third load is a no-op') + assert.strictEqual(store.listEvents({}).length, 1, 'event count stays at 1 after 3 loads') +}) + +// -- P0-1 fixture rubric ids match catalog ids ----------------------------- +test('rubric-fusion fixture covers every catalog rubric id', () => { + const seedSrc = fs.readFileSync(path.join(RENDERER, 'rubrics-seed.js'), 'utf8') + // Parse rubric names out of the inlined SKILL.md blobs. + const catalogIds = [] + for (const m of seedSrc.matchAll(/---\\nname: ([\w-]+)/g)) catalogIds.push(m[1]) + assert.ok(catalogIds.length >= 4, 'catalog seed has at least 4 rubrics') + const fixtureJson = JSON.parse(fs.readFileSync( + path.join(__dirname, '..', 'docs', 'rubric-fusion-fixture.json'), 'utf8')) + const fixtureIds = new Set((fixtureJson.rubrics || []).map(r => r.id)) + const missing = catalogIds.filter(id => !fixtureIds.has(id)) + assert.deepStrictEqual(missing, [], + 'every catalog rubric id has a matching fixture rubric def: missing=' + missing.join(',')) +}) + +// -- P0-2 tracing-page registers rubric-cell-jump listener ----------------- +test('tracing-page wires dsh:rubric-cell-jump → openDrill', () => { + const src = fs.readFileSync(path.join(RENDERER, 'tracing-page.js'), 'utf8') + assert.match(src, /addEventListener\(['"]dsh:rubric-cell-jump['"]/, + 'tracing-page must register dsh:rubric-cell-jump listener') + assert.match(src, /openDrill\(detail\.sessionId\)/, + 'listener must call openDrill(detail.sessionId) so the jump lands') +}) + +// -- P0-5 artifact-evolution shows blob-missing banner --------------------- +// Static source-string lock (matches artifact-evolution-board.test.js +// style for the artifacts.js IIFE — the module lives inside a closure +// and we already prove the runtime path there. Here we just prove the +// blob-missing banner path exists and gates on the right predicate). +test('renderEvolution emits blob-missing banner when any hop lacks blob', () => { + const src = fs.readFileSync(path.join(RENDERER, 'artifacts-board.js'), 'utf8') + assert.match(src, /artifact-evolution-banner/, 'source must reference the banner class') + assert.match(src, /missingBlobs\s*=\s*chain\.some/, + 'banner must gate on chain.some(missing-blob)') + const css = fs.readFileSync(path.join(RENDERER, 'style.css'), 'utf8') + assert.match(css, /\.artifact-evolution-banner\s*\{/, + 'style.css must style the new .artifact-evolution-banner selector') +}) + +// -- P0-4 artifact seed fixture is inline-reachable ------------------------ +test('artifact-board-seed.js inlines fixture on window.__dshArtifactBoardSeed', () => { + const src = fs.readFileSync(path.join(RENDERER, 'artifact-board-seed.js'), 'utf8') + assert.match(src, /window\.__dshArtifactBoardSeed\s*=/, 'seed must attach to window') + // Sanity — has multiple kinds and multiple versions. + assert.match(src, /"kind":\s*"svg"/, 'seed must include an svg artifact') + assert.match(src, /"kind":\s*"html"/, 'seed must include an html artifact') + assert.match(src, /"version":\s*3/, 'seed must include multi-version chain (v3+)') + // Wired into index.html so the browser actually loads it. + const html = fs.readFileSync(path.join(RENDERER, 'index.html'), 'utf8') + assert.match(html, /artifact-board-seed\.js/, 'index.html must include artifact-board-seed.js') +}) + +// -- P1 Lane A: graph binds click on fork nodes + user nodes --------------- +test('chat-session-graph binds click on actionable non-turn nodes', () => { + const src = fs.readFileSync(path.join(RENDERER, 'chat-session-graph.js'), 'utf8') + assert.match(src, /kind === ['"]fork['"] && node\.childSessionId/, + 'graph must bind click on fork nodes when childSessionId is present') + assert.match(src, /kind === ['"]user['"] && node\.seq/, + 'graph must bind click on user nodes when seq is present') +}) + +// -- P1 Lane A: renderer stores chatSelectedTurnId + wires user rows ------- +test('renderer propagates chatSelectedTurnId and wires user rows', () => { + const src = fs.readFileSync(path.join(RENDERER, 'renderer.js'), 'utf8') + assert.match(src, /state\.chatSelectedTurnId/, 'renderer must store chatSelectedTurnId in state') + assert.match(src, /row\.kind === ['"]user['"] && row\.seq/, + 'renderer drawer onSelect must handle user rows (kind==="user" && seq)') +})