spexcode 0.2.1 → 0.2.3
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/README.md +158 -103
- package/package.json +1 -1
- package/spec-cli/bin/spex.mjs +24 -1
- package/spec-cli/src/attach.ts +50 -0
- package/spec-cli/src/cli.ts +217 -64
- package/spec-cli/src/client.ts +47 -9
- package/spec-cli/src/{self.ts → doctor.ts} +26 -25
- package/spec-cli/src/guide.ts +79 -21
- package/spec-cli/src/harness.ts +53 -29
- package/spec-cli/src/help.ts +137 -49
- package/spec-cli/src/index.ts +31 -11
- package/spec-cli/src/issues.ts +48 -21
- package/spec-cli/src/layout.ts +3 -5
- package/spec-cli/src/lint.ts +34 -5
- package/spec-cli/src/localIssues.ts +44 -60
- package/spec-cli/src/materialize.ts +4 -2
- package/spec-cli/src/mentions.ts +22 -1
- package/spec-cli/src/pty-bridge.ts +39 -4
- package/spec-cli/src/ranker.ts +31 -12
- package/spec-cli/src/search.bench.mjs +30 -7
- package/spec-cli/src/search.ts +39 -0
- package/spec-cli/src/sessions.ts +160 -69
- package/spec-cli/src/specs.ts +16 -4
- package/spec-cli/src/supervise.ts +30 -6
- package/spec-cli/src/tree.ts +118 -0
- package/spec-cli/templates/hooks/post-merge +2 -2
- package/spec-cli/templates/hooks/pre-commit +34 -15
- package/spec-cli/templates/hooks/prepare-commit-msg +8 -1
- package/spec-cli/templates/spexcode.json +7 -0
- package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +1 -0
- package/spec-dashboard/dist/assets/Dashboard-Dlg78cbC.js +27 -0
- package/spec-dashboard/dist/assets/EvalsPage-CDxc1-in.js +3 -0
- package/spec-dashboard/dist/assets/FoldToggle-B5leylLf.js +1 -0
- package/spec-dashboard/dist/assets/IssuesPage-C2yFXiO-.js +1 -0
- package/spec-dashboard/dist/assets/MobileApp-RHNECU6x.js +1 -0
- package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +32 -0
- package/spec-dashboard/dist/assets/SessionInterface-YLD6IOmC.js +71 -0
- package/spec-dashboard/dist/assets/SessionWindow-CmKtpNUX.js +9 -0
- package/spec-dashboard/dist/assets/Settings-ZnOwskMZ.js +1 -0
- package/spec-dashboard/dist/assets/index-BdRQfrkR.js +41 -0
- package/spec-dashboard/dist/assets/index-DEc5Ru3l.css +1 -0
- package/spec-dashboard/dist/index.html +2 -2
- package/spec-yatsu/src/cli.ts +128 -26
- package/spec-yatsu/src/evaltab.ts +7 -6
- package/spec-yatsu/src/filing.ts +6 -3
- package/spec-yatsu/src/proof.ts +10 -0
- package/spec-yatsu/src/scenariofresh.ts +100 -30
- package/spec-yatsu/src/sidecar.ts +25 -3
- package/spec-yatsu/src/timeline.ts +53 -23
- package/README.zh-CN.md +0 -135
- package/spec-dashboard/dist/assets/index-Ct_ubwrd.css +0 -32
- package/spec-dashboard/dist/assets/index-DehTZ-h9.js +0 -145
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--paper: #fdf6e3;--panel: #f4eeda;--panel2:#eee8d5;--line: #ded7bf;--ink: #586e75;--ink2: #073642;--muted: #93a1a1;--blue: #268bd2;--green: #859900;--orange:#cb4b16;--red: #dc322f;--yellow:#b58900;--magenta:#d33682;--cyan: #2aa198;--term-bg: #0d1117;--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace}:root[data-theme=dark]{--paper: #0d1117;--panel: #161b22;--panel2:#21262d;--line: #30363d;--ink: #c9d1d9;--ink2: #f0f6fc;--muted: #8b949e;--blue: #58a6ff;--green: #3fb950;--orange:#f0883e;--red: #f85149;--yellow:#d29922;--magenta:#bc8cff;--cyan: #39c5cf;--term-bg: #0d1117}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--paper);color:var(--ink);font-family:var(--mono);font-size:13px;-webkit-font-smoothing:antialiased}*{scrollbar-color:var(--line) transparent;scrollbar-width:thin}::-webkit-scrollbar{width:9px;height:9px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--line);border-radius:6px;border:2px solid var(--panel)}::-webkit-scrollbar-thumb:hover{background:var(--muted)}::-webkit-scrollbar-corner{background:transparent}.app{display:flex;height:100vh}.app-main{flex:1;min-width:0;display:flex;position:relative}.page-graph{flex:1;min-width:0;display:grid;grid-template-columns:1fr var(--fp-w, 250px);position:relative}.side-rail{flex:0 0 52px;display:flex;flex-direction:column;align-items:center;gap:6px;padding:10px 0;background:var(--panel);border-right:1px solid var(--line)}.rail-spacer{flex:1}.rail-btn{width:36px;height:36px;display:flex;align-items:center;justify-content:center;background:none;border:none;border-radius:9px;cursor:pointer;color:var(--muted)}.rail-btn:hover{background:var(--panel2);color:var(--ink2)}.rail-btn.on{background:color-mix(in srgb,var(--blue) 14%,transparent);color:var(--blue)}.page-pane{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;background:var(--paper)}.page-evals,.page-issues{overflow:hidden;padding:10px 14px}.page-settings{overflow:auto;flex-direction:row;justify-content:center}.settings-body{width:min(720px,94%);padding:26px 0 60px}.page-title{font-size:17px;font-weight:700;color:var(--ink2);margin:6px 2px 14px}.pane-resizer{cursor:col-resize;z-index:6;position:relative}.pane-resizer:after{content:"";position:absolute;top:0;bottom:0;left:50%;width:3px;margin-left:-1.5px;background:transparent;border-radius:2px}.pane-resizer:hover:after,body.is-resizing .pane-resizer:after{background:color-mix(in srgb,var(--blue) 55%,transparent)}.page-graph>.pane-resizer{position:absolute;top:0;bottom:0;width:9px;right:calc(var(--fp-w, 250px) - 4px)}.si-resizer{flex:0 0 7px;margin:0 -3.5px}body.is-resizing,body.is-resizing *{cursor:col-resize!important;-webkit-user-select:none!important;user-select:none!important}.app.kbd-mode,.app.kbd-mode *{cursor:none!important}.app.kbd-mode .react-flow,.app.kbd-mode .react-flow *{pointer-events:none!important}.graph{position:relative;background:var(--paper);border-right:1px solid var(--line)}.react-flow__attribution{display:none}.react-flow__edge-path{stroke-dasharray:0}.move-edge .react-flow__edge-path{stroke-linecap:round}.hud{position:absolute;top:16px;left:18px;z-index:5;display:flex;flex-direction:row;align-items:center;gap:8px;pointer-events:none}.brand{color:var(--ink2);font-weight:700;letter-spacing:.5px}.hud .hud-help{align-self:center}.hud-help{pointer-events:auto;align-self:flex-start;cursor:pointer;width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:13px;line-height:1;color:var(--blue);background:var(--paper);border:1px solid var(--line);border-radius:4px}.hud-help:hover{border-color:var(--blue);color:var(--orange)}.lock-hint{position:absolute;top:16px;left:50%;transform:translate(-50%);z-index:6;display:flex;align-items:center;gap:10px;max-width:min(48vw,520px);padding:5px 8px 5px 11px;background:var(--paper);border:1px solid var(--line);border-left:4px solid var(--ov, var(--blue));border-radius:6px;box-shadow:0 6px 22px #657b8329;font-family:var(--mono);font-size:12px;color:var(--ink)}.lock-hint-lead{font-weight:700;flex:none;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lock-hint-body{color:var(--ink2);flex:1;min-width:0}.lock-hint kbd{border:1px solid var(--line);border-radius:3px;padding:0 5px;margin:0 1px;color:var(--blue);font-family:var(--mono);font-size:11px}.lock-hint-release{flex:none;cursor:pointer;padding:1px 8px;background:var(--paper);border:1px solid var(--line);border-radius:4px;color:var(--muted);font-family:var(--mono);font-size:11px}.lock-hint-release:hover{border-color:var(--blue);color:var(--orange)}.board-stats{position:absolute;bottom:14px;left:18px;z-index:5;display:flex;align-items:center;gap:5px;flex-wrap:wrap;max-width:calc(100% - 36px);padding:4px 9px;background:var(--paper);border:1px solid var(--line);border-radius:6px;box-shadow:0 4px 18px #657b831f;font-family:var(--mono);font-size:12px;color:var(--ink2)}.bstat-total{font-weight:700;color:var(--ink);padding-right:1px}.bstat{display:inline-flex;align-items:center;gap:3px;padding:1px 4px;border:0;border-radius:4px;background:none;font-family:var(--mono);font-size:12px;line-height:1.4;color:inherit;cursor:pointer}.bstat:hover:not(:disabled){background:var(--panel2);color:var(--ink)}.bstat:disabled{opacity:.38;cursor:default}.bstat-dot{width:7px;height:7px;border-radius:2px;flex:none}.bstat-drift{color:var(--yellow)}.bstat-issue{color:var(--magenta)}.bstat-sep{width:1px;align-self:stretch;margin:2px 3px;background:var(--line)}.board-stats .score-badge{width:13px;height:13px;font-size:8px}.board-stats .score-badge.empty{opacity:.6}.react-flow__node{transition:opacity .18s ease,transform .2s ease}.react-flow__node.is-far{opacity:.35}.react-flow__controls{box-shadow:none;border:1px solid var(--line);border-radius:4px;overflow:hidden}.react-flow__controls-button{background:var(--panel2);border-bottom:1px solid var(--line);color:var(--ink2)}.react-flow__controls-button:hover{background:var(--panel)}.react-flow__controls-button svg{fill:var(--ink)}.spec-node{position:relative;display:flex;flex-direction:column;justify-content:center;gap:3px;width:176px;min-height:46px;padding:5px 9px;background:var(--panel);border:1px solid var(--line);border-radius:4px;transition:border-color .12s}.spec-node.focused{border-color:var(--blue);outline:2px solid var(--blue);outline-offset:-1px}.spec-node.active.focused{border-color:var(--orange);outline-color:var(--orange)}.spec-node.drift.focused{border-color:var(--yellow);outline-color:var(--yellow)}.node-row1{display:flex;align-items:center;gap:7px}.node-row2{display:flex;align-items:center;gap:7px;min-height:18px}.node-dot{position:relative;width:7px;height:7px;border-radius:2px;flex:none}.pulse{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:2px;animation:pulse 1.6s ease-out infinite}@keyframes pulse{0%{transform:scale(1);opacity:.6}to{transform:scale(2.6);opacity:0}}.node-title{font-size:12px;font-weight:600;color:var(--ink2);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.node-ago{font-size:10px;color:var(--muted);flex:none;white-space:nowrap}.node-ver{font-size:10px;color:var(--muted);flex:none}.node-expand{position:absolute;right:-7px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:1px;font-size:9px;font-weight:700;line-height:1;color:var(--muted);background:var(--paper);border:1px solid var(--line);border-radius:7px;padding:2px 4px;pointer-events:none}.spec-node.focused .node-expand{color:var(--blue);border-color:var(--blue)}.node-editors{display:flex;align-items:center;gap:3px}.av-more{font-size:9px;font-weight:700;color:var(--muted);margin-left:1px}.avatar{display:inline-flex;border-radius:50%;box-shadow:0 0 0 1.5px var(--panel)}.av-face{display:inline-flex;align-items:center;justify-content:center;line-height:1;font-weight:700;overflow:hidden}.av-circle{border-radius:50%}.av-rounded{border-radius:5px}.av-square{border-radius:1px}.av-hex{border-radius:50%/18%;clip-path:polygon(25% 5%,75% 5%,100% 50%,75% 95%,25% 95%,0 50%)}.av-st-working{box-shadow:0 0 0 1.5px var(--panel),0 0 0 3px var(--green)}.av-st-idle{box-shadow:0 0 0 1.5px var(--panel),0 0 0 3px var(--muted)}.av-st-offline{box-shadow:0 0 0 1.5px var(--panel),0 0 0 3px var(--line);opacity:.55}.react-flow .react-flow__handle{opacity:0;pointer-events:none;background:transparent;border:none;width:5px;height:5px;min-width:0;min-height:0;border-radius:1px}.side{padding:18px 18px 24px;overflow-y:auto;background:var(--panel);border-left:1px solid var(--line)}.trail{display:flex;flex-wrap:wrap;align-items:center;gap:2px;margin-bottom:16px;font-size:11px}.trail .sep{color:var(--muted);margin:0 3px}.crumb{background:none;border:none;padding:1px 3px;cursor:pointer;color:var(--muted);font-family:var(--mono);font-size:11px}.crumb:hover{color:var(--blue)}.crumb.here{color:var(--ink2);font-weight:700}.side h1{font-size:17px;color:var(--ink2);margin:0 0 8px}.side h2{font-size:11px;color:var(--muted);margin:24px 0 10px;font-weight:500}.side h2:first-child{margin-top:0}.stats{display:flex;flex-direction:column;border:1px solid var(--line);border-radius:4px;overflow:hidden}.stats div{display:flex;justify-content:space-between;font-size:12px;padding:7px 10px}.stats div:nth-child(odd){background:var(--panel2)}.stats span{color:var(--muted)}.stats b{color:var(--ink2);font-weight:600}.c-merged{color:var(--green)}.c-active{color:var(--orange)}.c-drift{color:var(--yellow)}.c-pending{color:var(--muted)}.badge{display:inline-block;font-size:11px;padding:2px 0;border:none;background:none}.badge.merged{color:var(--green)}.badge.active{color:var(--orange)}.badge.drift{color:var(--yellow)}.badge.pending{color:var(--muted)}.desc{color:var(--ink);line-height:1.55;font-size:12.5px;margin:12px 0 4px}.meta{margin-top:14px;display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:4px;overflow:hidden}.meta div{display:flex;justify-content:space-between;font-size:12px;padding:7px 10px}.meta div:nth-child(odd){background:var(--panel2)}.meta span{color:var(--muted)}.meta b{color:var(--ink2);font-weight:600}.peek-btn{margin-top:22px;width:100%;padding:9px;background:var(--paper);color:var(--blue);border:1px solid var(--blue);border-radius:4px;font-family:var(--mono);font-size:13px;font-weight:600;cursor:pointer}.peek-btn:hover:not(:disabled){background:var(--blue);color:var(--paper)}.peek-btn:disabled{color:var(--muted);border-color:var(--line);cursor:default}.ov-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:20;background:#657b8340;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center}.ov-panel{width:min(900px,90vw);height:min(600px,84vh);background:var(--paper);border:1px solid var(--blue);border-radius:6px;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 12px 40px #657b8340}.ov-head{display:flex;align-items:center;gap:14px;padding:7px 12px;background:var(--panel2);border-bottom:1px solid var(--line)}.ov-title{font-size:13px;font-weight:700;color:var(--ink2)}.ov-tabs{display:flex;gap:4px}.ov-tab{background:none;border:1px solid transparent;border-radius:4px;padding:3px 8px;cursor:pointer;color:var(--muted);font-family:var(--mono);font-size:12px}.ov-tab:hover{color:var(--ink2)}.ov-tab.on{color:var(--ink2);border-color:var(--blue);background:var(--paper)}.ov-tab kbd{font-family:var(--mono);font-size:10px;color:var(--blue);border:1px solid var(--line);border-radius:2px;padding:0 3px;margin-right:3px;background:var(--paper)}.ov-tab-counts{display:inline-flex;gap:3px;margin-left:5px;vertical-align:baseline}.ovc{font-family:var(--mono);font-size:9px;font-weight:700;line-height:1.5;padding:0 4px;border-radius:3px;border:1px solid var(--line)}.ovc.st-open{color:var(--green);border-color:color-mix(in srgb,var(--green) 45%,transparent)}.ovc.st-closed{color:var(--magenta);border-color:color-mix(in srgb,var(--magenta) 45%,transparent)}.ovc.st-edit{color:var(--blue);border-color:color-mix(in srgb,var(--blue) 45%,transparent)}.ov-hint{margin-left:auto;font-size:11px;color:var(--muted)}.ov-body{flex:1;min-width:0;overflow:hidden;display:flex}.pane-doc{flex:1;padding:22px 26px;overflow:auto}.pane-doc h1{font-size:18px;color:var(--ink2);margin:0 0 14px}.pane-doc blockquote{margin:0 0 16px;padding:8px 14px;border-left:3px solid var(--blue);background:var(--panel2);color:var(--ink);line-height:1.6;font-size:13px}.pane-doc .doc-stat{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:0 0 16px;padding:6px 10px;background:var(--panel2);border:1px solid var(--line);border-radius:5px}.stat-status{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.stat-dot{width:7px;height:7px;border-radius:2px;flex:none;background:var(--muted)}.stat-status.st-merged{color:var(--green)}.stat-status.st-merged .stat-dot{background:var(--green)}.stat-status.st-active{color:var(--orange)}.stat-status.st-active .stat-dot{background:var(--orange)}.stat-status.st-drift{color:var(--yellow)}.stat-status.st-drift .stat-dot{background:var(--yellow)}.stat-status.st-pending{color:var(--muted)}.stat-status.st-pending .stat-dot{background:var(--muted)}.stat-chip{display:inline-flex;align-items:center;gap:4px;font-size:10.5px;color:var(--ink2);background:var(--paper);border:1px solid var(--line);border-radius:3px;padding:1px 6px}.stat-chip.stat-drift{color:var(--yellow);font-weight:700;border-color:color-mix(in srgb,var(--yellow) 45%,transparent)}.stat-sess{margin-left:auto;display:inline-flex;align-items:center;gap:5px;font-size:10.5px;color:var(--muted);min-width:0}.stat-sess b{color:var(--ink2);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pane-doc .doc-gov{margin:0 0 16px}.doc-gov-h{display:block;font-size:10.5px;color:var(--muted);margin-bottom:6px}.doc-gov-h b{color:var(--ink2);font-weight:700}.doc-gov-files{display:flex;flex-wrap:wrap;gap:5px}.gov-f{font-size:11px;color:var(--blue);background:var(--panel2);border:1px solid color-mix(in srgb,var(--blue) 22%,var(--line));padding:1px 7px;border-radius:3px}.doc-gov.prose .doc-gov-h{font-style:italic}.doc-body{border-top:1px solid var(--line);padding-top:14px;font-size:13px;line-height:1.65;color:var(--ink)}.doc-body p{margin:0 0 10px}.pane-loading{display:flex;align-items:center;justify-content:center;padding:48px 0}.spinner{width:22px;height:22px;border-radius:50%;border:2px solid var(--line);border-top-color:var(--ink2);animation:si-attach-spin .7s linear infinite}.doc-body ul{margin:0 0 10px;padding-left:18px}.doc-body li{margin:2px 0}.doc-body code{font-size:12px;background:var(--panel2);color:var(--ink2);padding:0 4px;border-radius:3px}.doc-body strong{color:var(--ink2)}.doc-h{font-size:12px;color:var(--ink2);font-weight:700;margin:14px 0 6px}.doc-pre{margin:0 0 10px;padding:10px 12px;background:var(--panel2);border-radius:4px;overflow-x:auto;font-size:12px;line-height:1.5}.doc-pre code{background:none;padding:0;color:var(--ink)}.doc-link{color:var(--blue)}.doc-table{width:100%;margin:0 0 12px;border-collapse:collapse;font-size:12px;display:block;overflow-x:auto}.doc-table th,.doc-table td{border:1px solid var(--line);padding:5px 9px;text-align:left;vertical-align:top}.doc-table th{background:var(--panel2);color:var(--ink2);font-weight:700}.doc-table code{white-space:nowrap}.spec-parts{display:flex;flex-direction:column;gap:14px;margin-top:14px}.spec-part{border:1px solid var(--line);border-radius:6px;border-left-width:3px;overflow:hidden}.spec-part.part-raw{border-left-color:var(--green)}.spec-part.part-expanded{border-left-color:var(--blue)}.spec-part.part-state{border-left-color:var(--orange)}.part-head{display:flex;align-items:baseline;gap:10px;padding:8px 12px;background:var(--panel2);flex-wrap:wrap}.part-title{font-size:12px;font-weight:700;color:var(--ink2);text-transform:uppercase;letter-spacing:.04em}.part-owner{font-size:10px;font-weight:700;padding:1px 7px;border-radius:9px;text-transform:uppercase;letter-spacing:.03em}.part-owner.owner-human{background:#8599002e;color:var(--green)}.part-owner.owner-agent{background:#268bd22e;color:var(--blue)}.part-note{font-size:11px;color:var(--muted);margin-left:auto}.part-body{padding:4px 14px 12px}.part-body .doc-body{border-top:none;padding-top:8px}.cs-sub+.cs-sub{margin-top:6px}.cs-label{font-size:11px;font-weight:700;color:var(--orange);text-transform:uppercase;letter-spacing:.04em;margin:12px 0 0}.ver-row{padding:10px 0;border-bottom:1px solid var(--line)}.ver-row.latest{border-left:2px solid var(--blue);padding-left:12px;margin-left:-14px}.rec-toggle{display:block;width:100%;text-align:left;background:none;border:0;padding:0;margin:0;cursor:pointer;font:inherit;color:inherit}.rec-caret{color:var(--muted);font-size:9px;width:9px;flex:none}.rec-head{display:flex;align-items:baseline;gap:10px;font-size:12px}.rec-v{color:var(--ink2);font-weight:700}.rec-hash{color:var(--muted);font-size:11px}.rec-date{color:var(--muted)}.rec-diff{margin-left:auto;display:flex;gap:8px;font-size:12px}.rec-add{color:var(--green)}.rec-del{color:var(--orange)}.rec-msg{color:var(--ink2);font-size:13px;margin-top:5px;word-break:break-word}.rec-sub{color:var(--muted);font-size:11px;margin-top:3px}.rec-msg.muted{color:var(--muted)}.rec-evidence{margin:12px 0 2px;padding:14px 0 0;border-top:1px solid var(--line)}.ev-note{font-size:11px;color:var(--muted);text-align:center;margin-top:12px}.ev-difflabel{font-size:11px;color:var(--muted);margin-bottom:8px}.ev-diff{margin:0;font-family:var(--mono);font-size:11px;line-height:1.55;background:var(--panel2);border:1px solid var(--line);border-radius:5px;padding:8px 0;overflow-x:auto}.ev-diff .dl{padding:0 12px;white-space:pre}.dl-hunk{color:var(--blue);opacity:.75}.dl-add{color:var(--green);background:#8599001a}.dl-del{color:var(--orange);background:#cb4b161a}.dl-ctx{color:var(--muted)}.pane-hist{flex:1;min-width:0;padding:16px 20px;overflow:auto}.pane-hist.empty{color:var(--muted);font-size:13px}.pane-solo{flex:1;min-width:0;display:flex}.spec-node.has-overlay{border-color:var(--ov);box-shadow:0 0 0 2px color-mix(in srgb,var(--ov) 22%,transparent)}.spec-node.has-overlay.ov-dirty{border-style:dashed}.spec-node.has-overlay.focused{outline-color:var(--ov)}.spec-node.ghost{background:transparent;border-style:dashed;opacity:.72}.spec-node.deleted .node-title{text-decoration:line-through;color:var(--muted)}.drift-badge{flex:none;font-size:10px;font-weight:700;color:var(--yellow);border:1px solid var(--yellow);border-radius:3px;padding:0 3px;line-height:1.4}.issue-badge{flex:none;font-size:10px;font-weight:700;color:var(--magenta);border:1px solid var(--magenta);border-radius:3px;padding:0 3px;line-height:1.4;cursor:default}.score-badge{flex:none;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border-radius:50%;border:1.5px solid currentColor;font-size:9px;font-weight:900;line-height:1;cursor:default}.score-badge.pass{color:var(--green)}.score-badge.fail{color:var(--red)}.score-badge.stalePass,.score-badge.staleFail{color:var(--muted)}.score-badge.empty{color:var(--muted);opacity:.45}.scenario-count{flex:none;font-family:var(--mono);font-size:10px;font-weight:700;line-height:1.4;border:1px solid currentColor;border-radius:3px;padding:0 3px;cursor:default}.scenario-count.pass{color:var(--green)}.scenario-count.fail{color:var(--red)}.scenario-count.stalePass,.scenario-count.staleFail{color:var(--muted)}.scenario-count.empty{color:var(--muted);opacity:.7}.focus-panel{overflow:auto;background:var(--paper);display:flex;flex-direction:column;gap:18px;padding:16px 14px}.fp-head{display:flex;flex-direction:column;gap:3px}.fp-title{font-size:15px;font-weight:700;color:var(--ink2);word-break:break-word;line-height:1.25}.fp-desc{color:var(--ink);font-size:11px;line-height:1.4;margin-top:3px;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}.fp-sec{display:flex;flex-direction:column;gap:5px}.fp-sec-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);border-bottom:1px solid var(--line);padding-bottom:5px}.fp-count{font-family:var(--mono);font-size:10px}.fp-count.pass{color:var(--green)}.fp-count.fail{color:var(--red)}.fp-count.stalePass,.fp-count.staleFail,.fp-count.empty{color:var(--muted)}.fp-empty{color:var(--muted);font-size:11px;font-style:italic;padding:3px 2px}.fp-scenario{position:relative;display:block;width:100%;padding:6px;text-align:left;background:none;border:none;border-radius:5px;font:inherit;color:inherit;cursor:pointer}.fp-scenario:hover{background:var(--panel2)}.fp-sc-mark{position:absolute;left:6px;top:6px;width:12px;text-align:center;font-weight:900;font-size:11px;line-height:1.5}.fp-scenario.pass .fp-sc-mark{color:var(--green)}.fp-scenario.fail .fp-sc-mark{color:var(--red)}.fp-scenario.stalePass .fp-sc-mark,.fp-scenario.staleFail .fp-sc-mark{color:var(--muted)}.fp-scenario.empty .fp-sc-mark,.fp-scenario.missing .fp-sc-mark{color:var(--muted);opacity:.55}.fp-sc-body{display:flex;flex-direction:column;gap:3px;min-width:0}.fp-sc-name{padding-left:20px;font-family:var(--mono);font-size:11px;font-weight:700;color:var(--ink2)}.fp-sc-expected{color:var(--ink);font-size:11px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.fp-sc-code{color:var(--muted);font-family:var(--mono);font-size:9px;opacity:.85}.tag-chips{display:inline-flex;flex-wrap:wrap;gap:3px;min-width:0}.tag-chip{font-family:var(--mono);font-size:9px;line-height:1.5;color:var(--ink2);background:var(--paper);border:1px solid var(--line);border-radius:3px;padding:0 4px;white-space:nowrap}.search-item .tag-chips{flex:none}.fp-issue-counts{display:flex;gap:6px;font-size:9px}.focus-panel .issue-group-head{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:8px 2px 3px}.focus-panel .issue-card{padding:6px;border-radius:5px}.issue-card{display:block;max-width:100%;min-width:0;box-sizing:border-box;overflow:hidden;padding:5px 6px;border-radius:5px;text-decoration:none}.issue-card+.issue-card{margin-top:2px}.issue-card:hover{background:var(--panel2)}.issue-card-top{display:flex;align-items:center;gap:6px;min-width:0;margin-bottom:2px}.issue-num{font-size:10px;font-weight:700;color:var(--magenta);flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono)}.issue-state{font-size:8px;color:var(--muted);flex:none;text-transform:uppercase;letter-spacing:.04em;border:1px solid var(--line);border-radius:3px;padding:0 3px;line-height:1.5}.issue-state.st-open{color:var(--green);border-color:color-mix(in srgb,var(--green) 45%,transparent)}.issue-state.st-closed{color:var(--magenta);border-color:color-mix(in srgb,var(--magenta) 45%,transparent)}.issue-card-title{display:block;font-size:11px;line-height:1.35;color:var(--ink2);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.pane-issues{flex:1;min-width:0;padding:16px 20px;overflow:auto}.pane-issues.empty{color:var(--muted);font-size:13px}.pane-issues .issue-group-head{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 2px 6px}.pane-issues .issue-group-head.closed{margin-top:16px;border-top:1px solid var(--line);padding-top:14px}.pane-issues .issue-card{padding:7px 8px}.pane-issues .issue-card+.issue-card{margin-top:1px}.pane-issues .issue-card-title{font-size:12px}.pane-edit{flex:1;min-width:0;padding:16px 20px;overflow:auto}.pane-edit.empty{color:var(--muted);font-size:13px}.edit-rev{margin:0 0 18px}.edit-by{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--muted);margin-bottom:8px}.edit-by-label{font-weight:600}.edit-state{margin-left:auto;font-family:var(--mono);font-size:10px}.ov-marks{display:flex;gap:3px;flex:none;margin-left:2px}.ov-mark{font-size:11px;font-weight:700;line-height:1;color:var(--ov, var(--blue))}.ov-mark.ov-deleted{color:var(--orange)}.react-flow__node.ov-hot{opacity:1}.react-flow__node.ov-dim{opacity:.22}.sesswin{position:absolute;top:48px;left:16px;z-index:6;width:232px;max-height:min(80vh,calc(100vh - 112px));background:var(--paper);border:1px solid var(--line);border-radius:6px;box-shadow:0 6px 22px #657b8329;overflow-y:auto;overflow-x:hidden;font-size:12px}.sesswin-empty{padding:12px 10px;color:var(--muted);font-size:11px;line-height:1.5}.sesswin-empty kbd{border:1px solid var(--line);border-radius:3px;padding:0 4px;color:var(--blue)}.sess-row{display:flex;flex-wrap:wrap;align-items:center;column-gap:7px;row-gap:2px;width:100%;padding:7px 9px;background:none;border:none;border-bottom:1px solid var(--line);border-left:2px solid var(--ov, transparent);cursor:pointer;text-align:left;font-family:var(--mono);font-size:12px;color:var(--ink2);-webkit-user-select:none;user-select:none;transition:background .12s,border-left-width .12s}.sess-row:last-child{border-bottom:none}.sess-row:hover{background:var(--panel)}.sess-row.locked{background:color-mix(in srgb,var(--ov) 12%,var(--panel2));border-left-width:4px;color:var(--ink);font-weight:600}.sess-id{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sess-lock{font-size:10px;flex:none;line-height:1;filter:saturate(0)}.sess-meta{order:1;flex:0 0 100%;width:100%;min-width:0;margin-top:1px;display:flex;align-items:center;column-gap:7px;font-size:10px;line-height:1.3;opacity:.9;overflow:hidden}.sess-status{color:var(--muted);flex:none}.sess-ops{color:var(--ov, var(--blue));font-weight:700;flex:none}.sess-glyph{flex:none;width:12px;text-align:center;font-size:11px;line-height:1}.sess-lead{flex:none;align-self:stretch;display:inline-flex;align-items:stretch;--rail: color-mix(in srgb, var(--muted) 42%, transparent)}.sess-rail{flex:none;width:14px;position:relative;min-height:1.7em}.sess-rail.rail:before,.sess-rail.tee:before,.sess-rail.elbow:before{content:"";position:absolute;left:50%;top:0;bottom:0;border-left:1px solid var(--rail)}.sess-rail.elbow:before{bottom:50%}.sess-rail.tee:after,.sess-rail.elbow:after{content:"";position:absolute;left:50%;right:1px;top:50%;border-top:1px solid var(--rail)}.sess-fold{flex:none;display:inline-flex;align-items:center;justify-content:center;align-self:center;cursor:pointer;-webkit-user-select:none;user-select:none}.sess-fold.pod{min-width:16px;width:auto;height:14px;padding:0 4px;border-radius:999px;border:1px solid;font-size:9.5px;font-weight:700;line-height:1;color:var(--paper)}.sess-fold.pod.open{background:transparent}.sess-fold.pod:hover{filter:brightness(1.25)}.sesswin .sess-row{flex-wrap:nowrap}.sesswin .sess-meta{flex:none;width:auto;min-width:0;margin-top:0;margin-left:auto}.sesswin-zone{--zh: var(--muted);display:flex;align-items:center;gap:7px;padding:8px 9px 2px;font-size:9.5px;font-weight:700;letter-spacing:.9px;text-transform:uppercase;-webkit-user-select:none;user-select:none;color:var(--zh)}.sesswin-zone:after{content:"";flex:1;height:1px;background:var(--zh);opacity:.25}.sesswin-zone-need{--zh: var(--yellow)}.sesswin-zone-run{--zh: var(--green)}.sesswin-zone-offline{--zh: var(--muted)}.sess-menu{position:fixed;z-index:60;min-width:116px;background:var(--paper);border:1px solid var(--line);border-radius:5px;box-shadow:0 6px 22px #657b8338;overflow:hidden;padding:3px}.sess-menu-item{display:block;width:100%;text-align:left;background:none;border:none;border-radius:3px;padding:5px 9px;cursor:pointer;font-family:var(--mono);font-size:12px;color:var(--ink2)}.sess-menu-item:hover{background:var(--blue);color:var(--paper)}.sess-menu-item.danger{color:var(--orange)}.sess-menu-item.danger:hover{background:var(--orange);color:var(--paper)}.sess-menu-sep{height:1px;margin:3px 4px;background:var(--line)}.sess-menu-item.sess-menu-sess{display:flex;align-items:center;gap:6px}.sess-menu-item.sess-menu-sess .sess-glyph{width:11px}.sess-rename-modal{width:min(360px,92vw)}.sess-rename{display:flex;flex-direction:column;gap:10px;padding:4px 2px 2px}.sess-rename-input{font-family:var(--mono);font-size:13px;color:var(--ink2);background:var(--paper);border:1px solid var(--line);border-radius:4px;padding:7px 9px}.sess-rename-input:focus{outline:none;border-color:var(--blue)}.sess-rename-actions{display:flex;justify-content:flex-end;gap:8px}.sess-rename-btn{background:var(--paper);border:1px solid var(--line);color:var(--ink);border-radius:4px;padding:4px 12px;cursor:pointer;font-family:var(--mono);font-size:12px}.sess-rename-btn:hover{background:var(--panel)}.sess-rename-save{border-color:var(--blue);color:var(--blue)}.sess-rename-save:hover{background:var(--blue);color:var(--paper)}.sess-rename-save:disabled{opacity:.5;cursor:default}.sess-confirm{display:flex;flex-direction:column;gap:12px;padding:4px 2px 2px}.sess-confirm-msg{margin:0;color:var(--ink);font-size:12px;line-height:1.4}.sess-rename-btn.danger{border-color:var(--orange);color:var(--orange)}.sess-rename-btn.danger:hover{background:var(--orange);color:var(--paper)}.sess-rename-btn.danger:disabled{opacity:.5;cursor:default}.legend-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:60;background:#657b8340;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center}.legend{position:relative;width:min(660px,92vw);max-height:86vh;overflow-y:auto;background:var(--paper);border:1px solid var(--blue);border-radius:6px;box-shadow:0 12px 40px #657b8340;font-size:12px}.legend-head{position:sticky;top:0;display:flex;align-items:center;justify-content:space-between;padding:6px 9px;background:var(--panel2);border-bottom:1px solid var(--line)}.legend-title{color:var(--muted);font-size:11px;letter-spacing:.5px}.legend-close{background:none;border:none;color:var(--muted);cursor:pointer;display:inline-flex;align-items:center;line-height:1;padding:2px}.legend-close:hover{color:var(--orange)}.legend-body{padding:4px 10px 10px}.legend-sec{padding:8px 0;border-bottom:1px solid var(--line)}.legend-sec:last-child{border-bottom:none}.legend-h{font-size:11px;color:var(--ink2);font-weight:700;margin-bottom:6px}.legend-sub{color:var(--muted);font-weight:400}.keymap-keys{flex:none;width:132px;display:flex;flex-wrap:wrap;gap:4px}.legend kbd,.settings-body kbd{font-family:var(--mono);font-size:11px;color:var(--blue);line-height:1.4;border:1px solid var(--line);border-radius:3px;padding:0 4px;background:var(--paper)}.legend-row{display:flex;align-items:baseline;gap:8px;padding:3px 0}.legend-row .node-dot{position:relative;top:1px}.legend-name{color:var(--ink2);font-weight:600;flex:none;width:52px}.legend-desc{color:var(--ink);flex:1;line-height:1.4}.legend-glyph{color:var(--blue);font-weight:700;flex:none;width:18px;text-align:center}.legend-glyph.legend-ver{font-size:11px;color:var(--muted)}.legend .ov-mark{flex:none;width:18px;text-align:center}.legend .drift-badge{flex:none}.legend-ring{flex:none;width:22px;height:14px;border-radius:3px;background:var(--panel)}.legend-ring.ring-dashed{border:1px dashed var(--blue)}.legend-ring.ring-solid{border:1px solid var(--blue)}.legend-ring.ring-ghost{border:1px dashed var(--line);background:transparent;opacity:.72}.set-langs{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px}.set-lang{font-family:var(--mono);font-size:12px;color:var(--ink);cursor:pointer;border:1px solid var(--line);border-radius:4px;padding:4px 12px;background:var(--paper)}.set-lang:hover{border-color:var(--blue);color:var(--blue)}.set-lang.on{border-color:var(--blue);color:var(--blue);background:var(--panel2);font-weight:600}.set-hint{display:block;color:var(--muted);font-size:11px;line-height:1.4}.set-keys{display:flex;flex-direction:column;gap:2px;margin:2px 0 8px}.set-key-row{display:flex;align-items:center;gap:8px;padding:2px 0}.set-key-row .legend-desc{flex:1;font-size:12px}.bind-cell{flex:none;min-width:92px;min-height:24px;display:flex;flex-wrap:wrap;align-items:center;gap:4px;justify-content:center;cursor:pointer;background:var(--paper);border:1px solid var(--line);border-radius:4px;padding:2px 6px}.bind-cell:hover:not(.fixed){border-color:var(--blue)}.bind-cell.capturing{border-color:var(--blue);border-style:dashed;background:var(--panel2)}.bind-cell.custom{border-color:var(--blue)}.bind-cell.fixed{cursor:default;opacity:.6}.bind-cell kbd{border:none;padding:0 2px;background:transparent}.bind-hint{font-size:11px;color:var(--blue)}.set-foot{display:flex;align-items:center;justify-content:space-between;gap:12px}.set-reset{flex:none;font-family:var(--mono);font-size:11px;color:var(--muted);cursor:pointer;border:1px solid var(--line);border-radius:4px;padding:3px 10px;background:var(--paper)}.set-reset:hover{border-color:var(--red);color:var(--red)}.search-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:60;background:#657b8340;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:flex-start;justify-content:center;padding-top:12vh}.search-panel{width:min(92vw,clamp(640px,50vw,1100px));max-height:72vh;display:flex;flex-direction:column;overflow:hidden;background:var(--paper);border:1px solid var(--blue);border-radius:6px;box-shadow:0 14px 44px #657b8347;font-size:12px}.search-bar{display:flex;align-items:center;gap:8px;padding:9px 12px;border-bottom:1px solid var(--line);background:var(--panel2)}.search-icon{color:var(--muted);font-size:14px;flex:none}.search-input{flex:1;min-width:0;background:transparent;border:none;outline:none;color:var(--ink2);font-family:var(--mono);font-size:14px;line-height:1.4}.search-input::placeholder{color:var(--muted)}.search-results{margin:0;padding:4px;list-style:none;overflow-y:auto;overflow-x:hidden}.search-empty{padding:8px 10px;color:var(--muted)}.search-item{display:flex;align-items:baseline;gap:8px;padding:5px 8px;border-radius:3px;cursor:pointer;white-space:nowrap}.search-item.on{background:var(--panel2)}.search-item .node-dot{position:relative;top:1px}.search-kind{flex:none;font-size:9px;line-height:1;letter-spacing:.4px;text-transform:uppercase;padding:2px 4px;border-radius:3px;border:1px solid var(--line);color:var(--muted);position:relative;top:-1px}.search-kind.k-spec{color:var(--blue);border-color:color-mix(in srgb,var(--blue) 40%,var(--line))}.search-kind.k-session{color:var(--orange);border-color:color-mix(in srgb,var(--orange) 40%,var(--line))}.search-kind.k-issue{color:var(--green);border-color:color-mix(in srgb,var(--green) 40%,var(--line))}.search-kind.k-scenario{color:var(--cyan);border-color:color-mix(in srgb,var(--cyan) 40%,var(--line))}.search-title{color:var(--ink2);font-weight:600;flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}.search-path{color:var(--muted);font-size:11px;flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}.search-foot{padding:6px 12px;border-top:1px solid var(--line);background:var(--panel);color:var(--muted);font-size:11px}.si-page{flex:1;min-width:0;display:flex}.si-panel{flex:1;min-width:0;background:var(--paper);border:none;border-radius:0;overflow:hidden;display:flex}.si-list{flex:0 0 240px;border-right:1px solid var(--line);background:var(--panel);display:flex;flex-direction:column;overflow-y:auto}.si-list-unfold{flex:0 0 22px;display:flex;align-items:flex-start;justify-content:center;padding:9px 0 0;color:var(--muted);background:var(--panel);border:none;border-right:1px solid var(--line);cursor:pointer;min-height:0}.si-list-unfold:hover{color:var(--blue);background:var(--panel2)}.si-item{display:flex;flex-wrap:wrap;align-items:center;column-gap:7px;row-gap:2px;width:100%;padding:9px 12px;background:none;border:none;border-bottom:1px solid var(--line);border-left:2px solid var(--ov, transparent);cursor:pointer;text-align:left;font-family:var(--mono);font-size:12px;color:var(--ink2)}.si-item:hover{background:var(--panel2)}.si-item.on{background:color-mix(in srgb,var(--blue) 15%,var(--panel2));color:var(--ink2);font-weight:600;border-left-width:3px}.si-item.on:hover{background:color-mix(in srgb,var(--blue) 24%,var(--panel2))}.si-item.on:before{content:"❯";flex:none;color:var(--green);font-weight:700}.si-item{flex-wrap:nowrap;padding:5px 12px;font-size:11px}.si-item.on:before{content:none}.si-item .sess-meta{flex:none;width:auto;min-width:0;margin-top:0;margin-left:auto;opacity:1;overflow:visible}.si-item .sess-lock{color:var(--muted)}.si-item.on{display:block}.si-item.on .sess-meta{float:right;margin:0 0 0 8px}.si-item.on .sess-id{white-space:normal;overflow:visible}.si-zone{--zh: var(--muted);display:flex;align-items:center;gap:8px;padding:10px 12px 4px;font-size:10px;font-weight:700;letter-spacing:1px;text-transform:uppercase;-webkit-user-select:none;user-select:none;color:var(--zh)}.si-zone:after{content:"";flex:1;height:1px;background:var(--zh);opacity:.25}.si-zone-need{--zh: var(--yellow)}.si-zone-run{--zh: var(--green)}.si-zone-offline{--zh: var(--muted)}.si-toprow{display:flex;gap:6px;padding:8px 10px;border-bottom:1px solid var(--line)}.si-selbar{display:flex;align-items:center;gap:6px;padding:8px 10px;border-bottom:1px solid var(--line)}.si-selcount{flex:1;font-family:var(--mono);font-size:11px;color:var(--ink2)}.si-selbar .sess-rename-btn{padding:4px 10px;font-size:11px}.si-check{flex:none;width:13px;height:13px;border:1.5px solid var(--muted);border-radius:3px;box-sizing:border-box}.si-check.on{border-color:var(--blue);background:var(--blue);position:relative}.si-check.on:after{content:"";position:absolute;left:3.5px;top:1px;width:3px;height:6px;border:solid var(--paper);border-width:0 1.5px 1.5px 0;transform:rotate(45deg)}.si-item.picked{background:color-mix(in srgb,var(--blue) 12%,var(--panel2))}.si-item.picked:hover{background:color-mix(in srgb,var(--blue) 20%,var(--panel2))}.si-pill{flex:1;display:flex;align-items:center;justify-content:center;padding:7px 0;background:none;border:1px solid var(--line);border-radius:5px;cursor:pointer;font-family:var(--mono);line-height:1}.si-pill-glyph{font-size:15px;display:flex}.si-pill.new{color:var(--blue)}.si-pill:hover{background:var(--panel2);border-color:currentColor}.si-pill.new.on{background:color-mix(in srgb,var(--blue) 16%,var(--panel2));border-color:var(--blue)}.si-pill.search{color:var(--ink2)}.si-pill.search:hover{color:var(--blue)}.si-pill.forum{color:var(--ink2)}.si-pill.forum:hover{color:var(--blue)}.si-pill.forum.on{color:var(--blue);background:color-mix(in srgb,var(--blue) 16%,var(--panel2));border-color:var(--blue)}.fv-master{flex:1;min-height:0;display:grid;grid-template-columns:minmax(232px,280px) 1fr}.fv-master.folded{grid-template-columns:22px 1fr}.fv-list-col{position:relative;min-height:0;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid var(--line);background:var(--paper)}.fv-list-col>.fv-notice{flex:none}.fv-list-col>.fv-group{flex:1 1 auto;min-height:0;overflow-y:auto;border-top:0}.fv-list-col>.fv-scroll{flex:1 1 auto;min-height:0;overflow-y:auto}.fv-list-col>.fv-scroll .fv-group{border-top:0}.fv-list-col>.fv-scroll .fv-group-head{padding-right:34px}.fv-fold{position:absolute;top:8px;right:6px;z-index:3;display:grid;place-items:center;padding:0;color:var(--muted);background:var(--panel);border:1px solid var(--line);border-radius:6px;width:22px;height:22px;cursor:pointer}.fv-fold:hover{color:var(--blue);border-color:var(--blue)}.fv-fold-inline{display:inline-flex;align-items:center;justify-content:center;flex:none;padding:0;color:var(--muted);background:var(--panel);border:1px solid var(--line);border-radius:6px;width:22px;height:22px;cursor:pointer}.fv-fold-inline:hover{color:var(--blue);border-color:var(--blue)}.fv-unfold{display:flex;align-items:flex-start;justify-content:center;padding:9px 0 0;color:var(--muted);background:var(--panel);border:none;border-right:1px solid var(--line);cursor:pointer;min-height:0}.fv-unfold:hover{color:var(--blue);background:var(--panel2)}.fold-toggle-icon{width:15px;height:15px;display:block;flex:none}.fv-detail{min-height:0;min-width:0;overflow-y:auto;background:var(--paper)}.fv-group{display:flex;flex-direction:column}.fv-group+.fv-group{border-top:1px solid var(--line)}.fv-group-head{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:6px;flex-wrap:wrap;row-gap:5px;padding:9px 12px;background:var(--panel2);border-bottom:1px solid var(--line)}.fv-group-title{font-size:9px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:var(--ink2)}.fv-head-row{flex-basis:100%;display:flex;align-items:center;gap:6px}.fv-row,.ef-row{display:flex;align-items:center;gap:8px;width:100%;min-width:0;text-align:left;font:inherit;font-size:12px;color:var(--ink2);background:none;border:0;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent);padding:7px 12px;cursor:pointer}.fv-row:hover,.ef-row:hover{background:var(--panel2)}.fv-row.sel,.ef-row.sel{background:color-mix(in srgb,var(--blue) 12%,var(--paper));box-shadow:inset 2px 0 0 var(--blue)}.fv-row .fv-concern{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fv-note{margin:auto;padding:24px;color:var(--muted);font-family:var(--mono);font-size:12.5px;text-align:center}.fv-dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--muted)}.fv-dot.st-open{background:var(--blue)}.fv-dot.st-landed{background:var(--green)}.fv-dot.st-closed{background:color-mix(in srgb,var(--muted) 55%,transparent)}.fv-replies{flex:none;font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:0 6px;line-height:1.4}.fv-store{flex:none;font-family:var(--mono);font-size:9px;letter-spacing:.8px;text-transform:uppercase;color:var(--muted)}.fv-store-local{color:color-mix(in srgb,var(--blue) 65%,var(--muted))}.fv-concern{flex:1 1 auto;min-width:80px;color:var(--ink);font-weight:600;font-size:12px}.fv-status{flex:none;font-family:var(--mono);font-size:9px;border:1px solid var(--line);border-radius:3px;padding:1px 6px;color:var(--muted)}.fv-st-open{color:var(--blue);border-color:color-mix(in srgb,var(--blue) 40%,var(--line))}.fv-st-landed{color:var(--green);border-color:color-mix(in srgb,var(--green) 40%,var(--line))}.fv-by{flex:none;color:var(--muted);font-size:11px}.fv-originator{flex:none;display:inline-flex;align-items:center;gap:5px;font:inherit;font-size:11px;color:var(--muted);background:transparent;border:0;border-radius:4px;padding:1px 3px}.fv-originator-dot{width:7px;height:7px;border-radius:50%;flex:none}.fv-originator-who{color:var(--ink2);font-family:var(--mono);font-size:10.5px}.fv-originator.openable{cursor:pointer;background:color-mix(in srgb,var(--green) 8%,transparent)}.fv-originator.openable:hover{background:color-mix(in srgb,var(--green) 16%,transparent)}.fv-originator.openable:focus-visible{outline:2px solid var(--green);outline-offset:2px}.fv-originator.offline{opacity:.7}.fv-chips{flex:none;display:flex;flex-wrap:wrap;gap:5px}.fvd{height:100%;min-height:0;display:flex;flex-direction:column}.fvd-scroll{flex:1 1 auto;min-height:0;overflow-y:auto;padding:16px 22px 24px}.fvd-scroll>*{max-width:860px}.fvd-compose{flex:none;border-top:1px solid var(--line);padding:8px 16px 12px;background:var(--panel)}.fvd-compose .fv-compose{margin-top:0;padding-top:0;border-top:none;max-width:860px}.fvd-head{margin-bottom:4px}.fvd-concern{font-size:16px;font-weight:700;color:var(--ink);line-height:1.35}.fv-close-issue{flex:none;font-family:var(--mono);font-size:11px;color:var(--orange);background:var(--paper);border:1px solid color-mix(in srgb,var(--orange) 35%,var(--line));padding:4px 12px;border-radius:4px;cursor:pointer}.fv-close-issue:hover{background:var(--orange);color:var(--paper);border-color:var(--orange)}.fv-close-issue:disabled{opacity:.5;cursor:default}.fv-close-issue.fv-life-promote{color:var(--blue);border-color:color-mix(in srgb,var(--blue) 35%,var(--line))}.fv-close-issue.fv-life-promote:hover{background:var(--blue);color:var(--paper);border-color:var(--blue)}.fvd-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:12px}.fvd-body{font-size:13px}.fv-chip{font-family:var(--mono);font-size:10.5px;color:var(--blue);background:var(--panel2);border:1px solid color-mix(in srgb,var(--blue) 22%,var(--line));padding:1px 7px;border-radius:3px;cursor:pointer;line-height:1.5}.fv-chip:hover{background:var(--blue);color:var(--paper);border-color:var(--blue)}.fv-count{color:var(--muted);font-size:10.5px}.fv-link{color:var(--blue);font-size:10.5px;text-decoration:none}.fv-link:hover{text-decoration:underline}.fv-text{font-family:var(--mono);font-size:12px;line-height:1.6;color:var(--ink2);white-space:pre-wrap}.fv-reply{margin-top:10px;padding-left:12px;border-left:2px solid var(--line);border-radius:0 4px 4px 0;transition:background .15s,border-color .15s}.fv-reply.active{border-left-color:var(--green);background:color-mix(in srgb,var(--green) 7%,transparent)}.fv-reply.sel{border-left-color:var(--orange);background:color-mix(in srgb,var(--orange) 9%,transparent)}.fv-reply.remark.open{border-left-color:var(--orange);border-left-width:3px}.fv-reply.remark.resolved{opacity:.62;border-left-color:color-mix(in srgb,var(--green) 45%,var(--line))}.fv-remark-state{font-family:var(--mono);font-size:10px;padding:0 5px;border-radius:3px;margin-left:auto}.fv-remark-state.open{color:var(--orange);background:color-mix(in srgb,var(--orange) 12%,transparent);font-weight:600}.fv-remark-state.resolved{color:var(--green);background:color-mix(in srgb,var(--green) 12%,transparent)}.fv-remark-act{font-family:var(--mono);font-size:10px;padding:0 6px;border-radius:3px;cursor:pointer;line-height:1.6;background:var(--paper)}.fv-remark-act.resolve{color:var(--green);border:1px solid color-mix(in srgb,var(--green) 40%,var(--line))}.fv-remark-act.resolve:hover{background:var(--green);color:var(--paper);border-color:var(--green)}.fv-remark-act.retract{color:var(--muted);border:1px solid var(--line)}.fv-remark-act.retract:hover{background:var(--orange);color:var(--paper);border-color:var(--orange)}.fv-remark-act:disabled{opacity:.5;cursor:default}.fv-reply-meta{display:flex;align-items:center;gap:8px;margin-bottom:3px;font-family:var(--mono);font-size:10.5px}.fv-reply-by{color:var(--blue);font-weight:600}.fv-reply-at{color:var(--muted)}.fv-anchor{font-family:var(--mono);font-size:10.5px;color:var(--orange);background:color-mix(in srgb,var(--orange) 12%,transparent);border:1px solid color-mix(in srgb,var(--orange) 35%,var(--line));border-radius:3px;padding:1px 6px;cursor:pointer}.fv-anchor:hover{background:var(--orange);color:var(--paper)}.fv-anchor.static{cursor:default}.fv-anchor.static:hover{background:color-mix(in srgb,var(--orange) 12%,transparent);color:var(--orange)}.fv-anchor.static.degraded{color:var(--muted);background:transparent;border-style:dashed;border-color:color-mix(in srgb,var(--muted) 45%,var(--line))}.fv-anchor.static.degraded:hover{color:var(--muted);background:transparent}.fv-reply-media{display:flex;flex-direction:column;gap:6px;margin-top:6px}.fv-frames{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px}.fv-frames .an-image,.fv-frames .eval-video{height:48px;width:auto;max-width:none}.fv-anchor-btn{display:inline-flex;align-items:center;gap:4px;font-family:var(--mono);font-size:10.5px;color:var(--orange);background:var(--panel2);border:1px solid color-mix(in srgb,var(--orange) 35%,var(--line));border-radius:4px;padding:3px 10px;cursor:pointer}.fv-anchor-btn:hover{background:var(--orange);color:var(--paper)}.fv-new-btn{display:inline-flex;align-items:center;justify-content:center;color:var(--blue);background:color-mix(in srgb,var(--blue) 10%,var(--panel2));border:1px solid color-mix(in srgb,var(--blue) 32%,var(--line));padding:3px 12px;border-radius:6px;cursor:pointer;transition:color .12s,background .12s,border-color .12s}.fv-new-btn:hover{background:var(--blue);color:var(--paper);border-color:var(--blue)}.fv-new-modal{width:min(520px,92vw);overflow:visible}.fv-new-modal .legend-body{padding:0}.fv-hint{font-family:var(--mono);font-size:10.5px;color:var(--muted)}.fv-notice{padding:6px 16px;font-family:var(--mono);font-size:11px;color:var(--green);background:color-mix(in srgb,var(--green) 8%,var(--paper));border-bottom:1px solid var(--line)}.fv-error{margin:4px 0 8px;font-family:var(--mono);font-size:11px;color:var(--red)}.fv-actions .fv-error{margin:0}.fv-compose{margin-top:12px;padding-top:10px;border-top:1px dashed var(--line)}.fv-compose .fv-textarea{resize:none;min-height:56px;max-height:38vh}.fv-new-form{display:flex;flex-direction:column;gap:8px;padding:12px 16px;border-bottom:1px solid var(--line);background:var(--panel2)}.fv-store-pick{display:flex;align-items:center;gap:8px;align-self:flex-start;font-family:var(--mono);font-size:10.5px;color:var(--muted)}.fv-store-pick select{font-family:var(--mono);font-size:11px;color:var(--ink2);background:var(--paper);border:1px solid var(--line);border-radius:4px;padding:3px 7px}.fv-input,.fv-textarea{width:100%;box-sizing:border-box;font-family:var(--mono);font-size:12px;color:var(--ink);background:var(--paper);border:1px solid var(--line);border-radius:4px;padding:6px 8px;resize:vertical}.fv-input:focus,.fv-textarea:focus{outline:none;border-color:var(--blue)}.fv-tawrap{position:relative}.fv-tawrap .fv-textarea{display:block}.fv-actions{display:flex;align-items:center;gap:10px;margin-top:6px}.fv-cancel{margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--muted);background:var(--paper);border:1px solid var(--line);padding:4px 12px;border-radius:4px;cursor:pointer}.fv-cancel:hover{color:var(--orange);border-color:var(--orange)}.fv-cancel:disabled{opacity:.5;cursor:default}.fv-send{margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--paper);background:var(--blue);border:1px solid var(--blue);padding:4px 14px;border-radius:4px;cursor:pointer}.fv-cancel+.fv-send{margin-left:0}.fv-send:disabled{opacity:.5;cursor:default}.si-content{flex:1;min-width:0;display:flex;flex-direction:column;background:var(--paper)}.si-content.is-new{align-items:center;justify-content:center}.si-new-center{width:min(560px,86%);display:flex;flex-direction:column;align-items:center;gap:12px;margin-bottom:12vh;animation:si-rise .18s ease}.si-avatar{font-size:30px;color:var(--blue);letter-spacing:2px}.si-ask{font-size:15px;color:var(--ink2)}.si-inputwrap{position:relative;width:100%;display:flex;border:1px solid var(--blue);border-radius:6px;background:var(--panel);padding:10px 12px;transition:height .12s ease}.mention-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:5;margin:0;padding:4px;list-style:none;max-height:248px;overflow-y:auto;background:var(--paper);border:1px solid var(--blue);border-radius:4px;box-shadow:0 6px 18px #07364229;animation:si-rise .12s ease}.mention-menu.up{top:auto;bottom:calc(100% + 4px)}.mention-menu.fixed{position:fixed;top:auto;z-index:70}.mention-head{padding:3px 8px 5px;font-size:10.5px;color:var(--muted);letter-spacing:.02em;border-bottom:1px solid var(--line);margin-bottom:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mention-item{display:flex;align-items:baseline;gap:8px;padding:4px 8px;border-radius:3px;cursor:pointer;font-size:12px;white-space:nowrap}.mention-item.on{background:var(--panel2)}.mention-item.new .mention-id{color:var(--green);font-style:italic}.mention-dot{width:6px;height:6px;border-radius:50%;flex:none;align-self:center}.mention-id{color:var(--blue);flex:none}.mention-id .mention-hit{color:var(--ink2);font-weight:700}.mention-path{color:var(--muted);font-size:11px;min-width:0;overflow:hidden;text-overflow:ellipsis}.slash-name{color:var(--blue);flex:none}.slash-name .mention-hit{color:var(--ink2);font-weight:700}.slash-desc{color:var(--muted);font-size:11px;min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis}.slash-src{color:var(--muted);font-size:10.5px;flex:none;opacity:.7}.slash-src.src-user{color:var(--green);opacity:.85}.slash-src.src-project{color:var(--cyan);opacity:.85}.slash-src.src-skill{color:var(--magenta);opacity:.85}.slash-name.board{color:var(--sc);font-weight:700}.slash-src.src-board{color:var(--sc);opacity:.9}.slash-src.src-mutating{color:var(--yellow);opacity:.85}.slash-src.src-report{color:var(--cyan);opacity:.85}.si-input{flex:1;min-width:0;resize:none;background:transparent;border:none;outline:none;color:var(--ink2);font-family:var(--mono);font-size:13px;line-height:1.5;max-height:180px;overflow-y:hidden;transition:height .12s ease}.si-input::placeholder{color:var(--muted)}.si-hint{font-size:11px;color:var(--muted)}.si-hint code{color:var(--blue);background:var(--panel2);padding:0 4px;border-radius:3px}.si-agent-glyph{width:16px;height:16px;display:block;fill:currentColor}.si-launcher-picker{display:inline-flex;align-items:center;gap:6px}.si-launcher-label{font-size:11px;color:var(--ink2);text-transform:lowercase}.si-launcher-harness{display:inline-flex;align-items:center;color:var(--ink2)}.si-launcher-select{height:28px;padding:0 8px;cursor:pointer;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:999px;font-size:12px;transition:border-color .12s ease}.si-launcher-select:hover{border-color:var(--blue)}.si-content.is-session .si-term-body{animation:si-expand .22s ease}.si-content.is-session{--si-dock-h: 54px}.si-tabbar{flex:none;display:flex;align-items:stretch;gap:8px;padding:0 10px;background:var(--panel);border-bottom:1px solid var(--line)}.si-tabs{flex:none;display:flex;gap:2px}.si-tab{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:none;cursor:pointer;padding:8px 14px;font-family:var(--mono);font-size:12px;color:var(--muted);border-bottom:2px solid transparent;margin-bottom:-1px}.si-tab:hover{color:var(--ink2)}.si-tab.on{color:var(--ink2);border-bottom-color:var(--blue)}.si-th-name{flex:1;min-width:0;align-self:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);font-size:12px;color:var(--ink2)}.si-term-body{flex:1;min-height:0;overflow:hidden;padding:6px 10px 0;margin-bottom:var(--si-dock-h);background:var(--term-bg)}.si-line{font-size:12px;line-height:1.6;color:var(--ink2);white-space:pre-wrap;word-break:break-word}.si-line.muted{color:var(--muted)}.si-op{color:var(--blue);font-weight:700}.si-bottom{position:absolute;left:12px;right:12px;bottom:12px;z-index:2;display:flex;align-items:flex-end;gap:8px;padding:8px 10px;border:1px solid var(--line);border-radius:6px;background:var(--panel)}.si-prompt{color:var(--blue);flex:none}.si-bottom .si-input:disabled{color:var(--muted)}.si-bottom.err{border-color:var(--orange)}.si-send-err{flex:none;color:var(--orange);font-size:11px;white-space:nowrap}.si-attach{flex:none;align-self:flex-end;display:inline-flex;align-items:center;background:transparent;border:none;cursor:pointer;color:var(--muted);line-height:1.5;padding:0 2px}.si-attach:hover:not(:disabled){color:var(--blue)}.si-attach:disabled{opacity:.5;cursor:default}.si-attach-busy{animation:si-attach-spin .8s linear infinite}@keyframes si-attach-spin{to{transform:rotate(360deg)}}.si-inputwrap.dragover,.si-bottom.dragover{border-color:var(--green)}.si-attach-err{flex:none;align-self:center;color:var(--orange);font-size:11px;white-space:nowrap}@keyframes si-rise{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}@keyframes si-expand{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}.st-wrap{position:relative;width:100%;height:100%}.st-host{width:100%;height:100%;overflow:hidden}.st-copyhint{position:absolute;right:8px;bottom:7px;pointer-events:none;z-index:2;font-family:var(--mono);font-size:11px;line-height:1.25;letter-spacing:.2px;color:#cfe8e8;opacity:.85;background:#073642eb;border:1px solid rgba(131,148,150,.35);padding:3px 8px;border-radius:5px;transition:opacity .15s ease,color .15s ease,background .15s ease,border-color .15s ease}.st-wrap:hover .st-copyhint{opacity:1}.st-copyhint.copied{opacity:1;color:#fdf6e3;background:#859900f2;border-color:#859900f2}.st-copyhint.reconnecting{opacity:1;color:#fdf6e3;background:#cb4b16f2;border-color:#cb4b16f2;animation:st-recon-pulse 1.1s ease-in-out infinite}@keyframes st-recon-pulse{0%,to{opacity:1}50%{opacity:.55}}.st-host .xterm{width:100%!important;height:100%!important;background:var(--term-bg)!important}.st-host .xterm-screen{background:var(--term-bg)!important}.st-host .xterm-viewport{overflow-y:auto!important;overflow-x:hidden!important;background:var(--term-bg)!important}.si-bottom .si-input{color:var(--ink2)}.si-actions{margin-left:auto;display:flex;align-items:center;gap:6px}.si-act{background:var(--paper);border:1px solid var(--line);color:var(--ink2);border-radius:4px;padding:1px 9px;cursor:pointer;font-family:var(--mono);font-size:11px}.si-act:hover,.si-act.go{border-color:var(--blue);color:var(--blue)}.si-act.go:hover{background:var(--blue);color:var(--paper)}.si-act.big{padding:8px 18px;font-size:13px;margin-top:8px}.sc-red{--sc: var(--red)}.sc-green{--sc: var(--green)}.sc-yellow{--sc: var(--yellow)}.sc-cyan{--sc: var(--cyan)}.sc-muted{--sc: var(--muted)}.si-act.board{border-color:var(--sc);color:var(--sc)}.si-act.board:hover{background:var(--sc);color:var(--paper);border-color:var(--sc)}.si-act.board.type.on{background:var(--sc);color:var(--paper)}.si-act.board.type.suggest{animation:si-type-pulse 1.1s ease-in-out infinite}@keyframes si-type-pulse{0%,to{box-shadow:0 0 #b5890080}50%{box-shadow:0 0 0 3px #b5890000}}.si-bottom.type{border-color:var(--yellow);background:var(--panel2);cursor:pointer;gap:10px}.si-type-ind{color:var(--yellow);font-weight:700;flex:none}.si-type-help{color:var(--muted);font-size:11px}.si-offline{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;text-align:center;padding:24px}.si-offline-msg{font-size:15px;color:var(--ink2)}.si-offline-sub{font-size:12px;color:var(--muted);max-width:460px;line-height:1.6}.si-offline-sub code{color:var(--blue);background:var(--panel2);padding:0 4px;border-radius:3px}.si-offline-err{margin-top:10px;font-size:12px;color:var(--yellow);max-width:460px;line-height:1.55;border:1px solid var(--yellow);border-radius:4px;padding:8px 10px;background:var(--panel2)}.m-app{position:fixed;top:0;right:0;bottom:0;left:0;z-index:10;display:flex;flex-direction:column;background:var(--paper);color:var(--ink);font-family:var(--mono);-webkit-tap-highlight-color:transparent}.m-topbar{flex:none;display:flex;align-items:center;gap:8px;height:48px;padding:0 10px;padding-top:env(safe-area-inset-top,0);box-sizing:content-box;border-bottom:1px solid var(--line);background:var(--panel)}.m-back{flex:none;width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:26px;line-height:1;color:var(--blue);background:none;border:none;cursor:pointer}.m-back.ghost{visibility:hidden}.m-brand{font-weight:700;letter-spacing:.4px;color:var(--ink2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.m-main{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden;overscroll-behavior:contain}.m-specs{flex:1;min-height:0;display:flex;flex-direction:column}.m-crumbs{flex:none;display:flex;align-items:center;gap:2px;padding:7px 10px;overflow-x:auto;white-space:nowrap;border-bottom:1px solid var(--line);background:var(--paper)}.m-crumbs::-webkit-scrollbar{display:none}.m-crumb{display:inline-flex;align-items:center;gap:2px}.m-crumb-sep{color:var(--muted)}.m-crumb-btn{background:none;border:none;padding:2px 3px;font-family:var(--mono);font-size:12px;color:var(--muted);cursor:pointer}.m-crumb-btn.cur{color:var(--ink2);font-weight:700}.m-node{flex:1;min-height:0;display:flex;flex-direction:column}.m-node-head{flex:none;display:flex;align-items:center;flex-wrap:wrap;gap:6px 8px;padding:12px 14px 8px}.m-dot{flex:none;width:9px;height:9px;border-radius:50%}.m-node-title{font-size:16px;font-weight:700;color:var(--ink2)}.m-node-ver{font-size:12px;color:var(--muted)}.m-node-status{font-size:11px;color:var(--ink);padding:1px 6px;border:1px solid var(--line);border-radius:999px}.m-node-faces{display:inline-flex;gap:2px}.m-node-desc{flex:none;padding:0 14px 10px;color:var(--ink);font-size:13px;line-height:1.5}.m-tabs{flex:none;display:flex;gap:6px;padding:0 10px 8px;overflow-x:auto;white-space:nowrap;border-bottom:1px solid var(--line)}.m-tabs::-webkit-scrollbar{display:none}.m-tab{flex:none;padding:6px 12px;min-height:32px;cursor:pointer;font-family:var(--mono);font-size:12px;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:999px}.m-tab.on{color:var(--paper);background:var(--blue);border-color:var(--blue)}.m-pane-host{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}.m-pane-host>*{flex:1;min-height:0}.m-pane-host.fixed .pane-hist{height:100%}.m-pane-host .pane-doc{overflow:auto;-webkit-overflow-scrolling:touch;padding:14px}.m-pane-host .pane-doc>h1,.m-pane-host .pane-doc>blockquote{display:none}.m-pane-host .pane-issues,.m-pane-host .pane-edit{-webkit-overflow-scrolling:touch}.m-children{overflow:auto;-webkit-overflow-scrolling:touch}.m-row{display:flex;align-items:center;gap:9px;width:100%;min-height:52px;padding:10px 14px;text-align:left;cursor:pointer;background:var(--paper);border:none;border-bottom:1px solid var(--line);font-family:var(--mono);color:var(--ink)}.m-row:active{background:var(--panel)}.m-row[disabled]{opacity:.5}.m-row-title{flex:1;min-width:0;font-size:14px;color:var(--ink2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.m-row-faces{flex:none;display:inline-flex;gap:2px}.m-row-ver{flex:none;font-size:11px;color:var(--muted)}.m-row-kids{flex:none;font-size:11px;color:var(--blue)}.m-row-chev{flex:none;color:var(--muted);font-size:18px}.m-tabbar{flex:none;display:flex;border-top:1px solid var(--line);background:var(--panel);padding-bottom:env(safe-area-inset-bottom,0)}.m-tabbar-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;height:52px;cursor:pointer;background:none;border:none;font-family:var(--mono);font-size:13px;color:var(--muted)}.m-tabbar-btn.on{color:var(--blue);font-weight:700;box-shadow:inset 0 2px 0 var(--blue)}.m-tabbar-ico{font-size:13px}.m-tabbar-badge{min-width:16px;padding:0 4px;border-radius:999px;background:var(--orange);color:var(--paper);font-size:10px;line-height:16px;text-align:center}.m-sesslist{flex:1;min-height:0;overflow:auto;-webkit-overflow-scrolling:touch}.m-sess-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px 8px;width:100%;padding:12px 14px;text-align:left;cursor:pointer;background:var(--paper);border:none;border-bottom:1px solid var(--line);font-family:var(--mono)}.m-sess-row:active{background:var(--panel)}.m-sess-row .sess-id{flex:1;min-width:0;font-weight:700;color:var(--ink2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.m-sess-row .sess-meta{font-size:11px;line-height:1.4}.m-sess-row .sess-ops{color:var(--muted)}.m-sessdetail{flex:1;min-height:0;overflow:auto;-webkit-overflow-scrolling:touch}.m-sess-card{display:flex;align-items:center;gap:10px;padding:14px;border-bottom:1px solid var(--line)}.m-sess-meta{display:flex;flex-direction:column;gap:2px;min-width:0}.m-sess-name{font-size:16px;font-weight:700;color:var(--ink2)}.m-sess-status{font-size:12px;color:var(--muted)}.m-sess-activity{padding:10px 14px;color:var(--ink);font-size:13px;line-height:1.5;background:var(--panel);border-bottom:1px solid var(--line)}.m-sess-section{padding:12px 14px 6px;font-size:11px;font-weight:700;letter-spacing:.4px;color:var(--muted);text-transform:uppercase}.m-sessdetail .ov-mark{flex:none}.m-empty{padding:18px 14px;color:var(--muted);font-size:13px}.m-empty.big{flex:1;display:flex;align-items:center;justify-content:center;text-align:center}.pane-eval{flex:1;min-width:0;padding:16px 20px;overflow:auto}.pane-eval.empty{color:var(--muted);font-size:13px}.eval-todo-note{color:var(--muted);font-size:13px;margin:0 0 14px}.eval-declared-row .eval-expected{padding-left:23px;margin:4px 0 0}.eval-tracks{color:var(--muted);font-family:var(--mono);font-size:10px;opacity:.85}.eval-dangling-badge{color:var(--orange);margin-right:6px}.eval-dangling-name{text-decoration:line-through;color:var(--muted)}.eval-dangling-tag{font-family:var(--mono);font-size:10px;color:var(--orange);margin-left:8px;border:1px dashed color-mix(in srgb,var(--orange) 45%,var(--line));border-radius:3px;padding:0 5px}.eval-dangling-remarks{padding-left:23px;margin:6px 0 0}.eval-row{border-bottom:1px solid var(--line);padding:10px 0}.eval-row:last-child{border-bottom:0}.eval-head{display:flex;flex-direction:column;gap:4px;width:100%;text-align:left;background:none;border:0;padding:0;margin:0;font:inherit;color:inherit;cursor:pointer}.eval-top{display:flex;align-items:center;gap:8px}.eval-caret{flex:none;color:var(--muted);font-size:11px}.eval-scenario{font-size:13px;font-weight:600;color:var(--ink2)}.eval-meta{display:flex;align-items:center;gap:10px;font-size:11px;color:var(--muted)}.eval-sha{color:var(--blue);background:var(--panel2);padding:0 4px;border-radius:3px}.eval-verdict{flex:none;font-size:10px;font-weight:700;border-radius:3px;padding:0 4px;line-height:1.5}.eval-verdict.pass{color:var(--green);border:1px solid var(--green)}.eval-verdict.fail{color:var(--orange);border:1px solid var(--orange)}.eval-verdict.note{color:var(--blue);border:1px solid var(--blue);cursor:help}.eval-verdict.legacy{color:var(--muted);border:1px solid var(--line)}.eval-shot{margin:8px 0 2px;padding-left:19px}.eval-shot img,.eval-video{display:block;max-width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:4px}.eval-noimg{font-size:11px;color:var(--muted)}.eval-expected,.eval-note{font-size:12px;color:var(--ink2);margin:0 0 6px}.eval-expected-label{color:var(--muted);font-weight:600}.eval-transcript{font-family:var(--mono);font-size:11px;color:var(--ink2);background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:8px 10px;margin:0;max-height:360px;overflow:auto;white-space:pre-wrap;word-break:break-word}.eval-transcript.loading{color:var(--muted)}.eval-datawrap{border:1px solid var(--line);border-left:3px solid var(--acc, #6b46c1);border-radius:4px;overflow:hidden}.eval-datahead{font-family:var(--mono);font-size:10px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);background:var(--panel);padding:4px 10px;border-bottom:1px solid var(--line)}.eval-data{font-family:var(--mono);font-size:11px;color:var(--ink2);background:var(--panel2);padding:8px 10px;margin:0;max-height:360px;overflow:auto;white-space:pre-wrap;word-break:break-word}.eval-data.loading{color:var(--muted)}.proof-newtab{margin-left:auto;color:var(--blue);text-decoration:none;font-size:11px}.proof-newtab:hover{text-decoration:underline}.proof-iframe{flex:1;width:100%;border:0;background:#0b0e14}.ef-chipbar{display:flex;gap:6px;flex-wrap:wrap}.ef-chip{font:inherit;font-size:11px;color:var(--muted);background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:3px 10px;cursor:pointer;line-height:1.35;transition:color .12s,border-color .12s,background .12s}.ef-chip:hover{color:var(--ink2);border-color:color-mix(in srgb,var(--blue) 45%,var(--line))}.ef-chip.on{color:var(--paper);background:var(--blue);border-color:var(--blue)}.ef-close{font:inherit;color:var(--muted);background:none;border:0;cursor:pointer;padding:2px 6px}.ef-empty,.ef-miss{color:var(--muted);font-size:12px;padding:18px 8px}.ef-scenario{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;font-size:12px}.ef-node{flex:0 1 auto;max-width:58px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10.5px;opacity:.9}.ef-kind{flex:none;margin-left:auto;font-size:11px;color:var(--muted)}.ef-time{flex:none;color:var(--muted);font-size:10.5px;min-width:26px;text-align:right}.an-detail{height:100%;min-height:0;display:flex;flex-direction:column;padding:0}.an-head{flex:none;display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 16px;border-bottom:1px solid var(--line);background:var(--panel2)}.an-work{flex:1 1 auto;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,340px)}.an-stage-col{min-width:0;min-height:0;overflow-y:auto;padding:12px 16px 20px}.an-rail{min-width:0;min-height:0;display:flex;flex-direction:column;border-left:1px solid var(--line);background:var(--panel)}.an-rail .an-comments-head{flex:none;padding:10px 12px 8px;border-bottom:1px solid var(--line)}.an-rail-list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:4px 12px 8px}.an-rail-list .fv-reply{margin-top:8px}.an-rail-compose{flex:none;border-top:1px solid var(--line);padding:0 12px 10px;background:var(--panel)}.an-rail-compose .fv-compose{margin-top:8px;padding-top:0;border-top:none}@media (max-width: 900px){.an-detail{height:auto}.an-work{display:block}.an-stage-col{overflow:visible}.an-rail{border-left:none;border-top:1px solid var(--line)}.an-rail-list{overflow:visible}.an-rail-compose{position:sticky;bottom:0;z-index:2}}.an-meta{font-size:10.5px;color:var(--muted)}.an-image{display:block;max-width:100%;border:1px solid var(--line);border-radius:6px;background:var(--panel2);cursor:zoom-in}.lightbox{position:fixed;top:0;right:0;bottom:0;left:0;z-index:100;display:flex;align-items:center;justify-content:center;background:#000000d1;cursor:zoom-out}.lightbox img{max-width:96vw;max-height:96vh;object-fit:contain;border-radius:4px;box-shadow:0 8px 48px #0009}.an-prior-note{color:var(--muted)}.an-stale{color:var(--muted);font-family:var(--mono);font-size:11px}.an-stale b{color:var(--ink2);font-weight:700}.an-stale-files{opacity:.85}.an-title{font-weight:700;font-size:14px;color:var(--ink)}.an-node{font-size:11px;color:var(--muted)}.an-expected{font-size:12px;color:var(--ink2);padding:8px 0 0}.an-comments-head{font-size:10px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:var(--ink2)}.an-stage-col>*{max-width:1100px}.an-gallery{display:flex;flex-direction:column;gap:10px;margin:12px 0 0}.an-detail>.an-expected,.an-detail>.an-hint{padding:8px 16px 0}.an-stage{position:relative;margin:12px 0 0;cursor:crosshair}.an-video{display:block;width:100%;background:#000;border:1px solid var(--line);border-radius:6px 6px 0 0}.an-rect{position:absolute;border:2px solid var(--orange);border-radius:3px;background:#ff963c1f;pointer-events:none}.an-rect.live{border-style:dashed}.an-bigplay{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:54px;height:54px;border-radius:50%;background:#080c1280;border:1px solid rgba(255,255,255,.22);display:flex;align-items:center;justify-content:center;font-size:22px;color:#fff;pointer-events:none;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}.an-bar{display:flex;align-items:center;gap:10px;padding:6px 8px;background:var(--panel2);border:1px solid var(--line);border-top:none;border-radius:0 0 6px 6px}.an-play{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:30px;height:26px;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:5px;cursor:pointer}.an-play:hover{border-color:var(--blue)}.an-fs{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:30px;height:26px;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:5px;cursor:pointer}.an-fs:hover{border-color:var(--blue)}.an-player:fullscreen{display:flex;flex-direction:column;align-items:center;justify-content:center;background:#000}.an-player:fullscreen .an-stage{margin:0;max-width:100vw;max-height:calc(100vh - 38px)}.an-player:fullscreen .an-video{width:auto;max-width:100vw;max-height:calc(100vh - 38px);border:none;border-radius:0}.an-player:fullscreen .an-bar{width:min(100vw,1100px);border-radius:0}.an-player:-webkit-full-screen{display:flex;flex-direction:column;align-items:center;justify-content:center;background:#000}.an-player:-webkit-full-screen .an-stage{margin:0;max-width:100vw;max-height:calc(100vh - 38px)}.an-player:-webkit-full-screen .an-video{width:auto;max-width:100vw;max-height:calc(100vh - 38px);border:none;border-radius:0}.an-player:-webkit-full-screen .an-bar{width:min(100vw,1100px);border-radius:0}.an-time{flex:0 0 auto;font-family:var(--mono);font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums}.an-curstep{flex:0 0 auto;font-size:10.5px;color:var(--green);background:color-mix(in srgb,var(--green) 12%,transparent);border:1px solid color-mix(in srgb,var(--green) 35%,var(--line));border-radius:3px;padding:1px 7px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.an-seek{flex:1 1 auto;position:relative;height:18px;display:flex;align-items:center;cursor:pointer}.an-seek-trk{position:absolute;left:0;right:0;height:5px;background:var(--line);border-radius:5px}.an-seek-play{position:absolute;left:0;height:5px;background:var(--blue);border-radius:5px;transition:width .12s linear}.an-knob{position:absolute;width:12px;height:12px;background:var(--ink);border:2px solid var(--paper);border-radius:50%;transform:translate(-50%);box-shadow:0 1px 4px #0006;transition:left .12s linear}.an-band{position:absolute;top:2px;width:2px;height:14px;background:color-mix(in srgb,var(--green) 45%,transparent);transform:translate(-50%);pointer-events:none}.an-mk{position:absolute;top:-1px;width:10px;height:20px;padding:0;transform:translate(-50%);background:none;border:none;cursor:pointer}.an-mk:after{content:"";display:block;margin:0 auto;width:3px;height:20px;border-radius:2px;background:var(--orange);box-shadow:0 0 0 1px var(--paper);transition:transform .12s,background .12s}.an-mk:hover:after{transform:scaleX(1.8)}.an-mk.active:after{background:var(--green)}.an-mk.on:after{transform:scaleX(2);background:var(--orange);box-shadow:0 0 0 1px var(--paper),0 0 8px color-mix(in srgb,var(--orange) 60%,transparent)}.an-seek-hov{position:absolute;bottom:22px;transform:translate(-50%);background:var(--panel);border:1px solid var(--line);color:var(--ink);font-family:var(--mono);font-size:10.5px;padding:1px 6px;border-radius:4px;pointer-events:none;white-space:nowrap}.an-keys{font-family:var(--mono);font-size:10px;color:var(--muted);padding:4px 0 0;letter-spacing:.2px}.an-ruler{display:flex;flex-wrap:wrap;gap:4px;padding:8px 0 0}.an-step{font:inherit;font-size:11px;color:var(--ink2);background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:2px 8px;cursor:pointer}.an-step.on{border-color:var(--green);color:var(--ink)}.an-hint{color:var(--muted);font-size:11px;padding:8px 0 0}.an-flash{font-size:12px;color:var(--green);padding:4px 0 0}.an-verdict-badge{font-size:12px;font-weight:700;line-height:1}.an-verdict-badge.pass{color:var(--green)}.an-verdict-badge.fail{color:var(--orange)}.an-verdict-badge.legacy{color:var(--muted)}.an-ab{display:flex;align-items:center;gap:8px;margin-left:auto}.an-ab-nav{display:inline-flex;align-items:center;justify-content:center;line-height:1;color:var(--ink2);background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:2px 6px;cursor:pointer}.an-ab-nav:disabled{opacity:.35;cursor:default}.an-ab-track{display:flex;flex-wrap:wrap;gap:3px;align-items:center}.an-ab-pip{font:inherit;font-size:11px;font-weight:700;width:20px;height:20px;display:flex;align-items:center;justify-content:center;background:var(--panel2);border:1px solid var(--line);border-radius:4px;cursor:pointer;color:var(--muted)}.an-ab-pip.pass{color:var(--green)}.an-ab-pip.fail{color:var(--orange)}.an-ab-pip.on{outline:2px solid var(--blue);outline-offset:0;border-color:var(--blue)}.an-ab-pos{font-size:10.5px;color:var(--muted);margin-left:2px}.se-pane{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--paper)}.se-gates{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 14px;border-bottom:1px solid var(--line);background:var(--panel2)}.se-gate{font-family:var(--mono);font-size:10.5px;border:1px solid var(--line);border-radius:3px;padding:1px 7px;color:var(--muted)}.se-gate.ok{color:var(--green);border-color:color-mix(in srgb,var(--green) 40%,var(--line))}.se-gate.bad{color:var(--red);border-color:color-mix(in srgb,var(--red) 40%,var(--line))}.se-export{margin-left:auto;display:inline-flex;align-items:center;color:var(--blue);text-decoration:none}.se-export:hover{color:color-mix(in srgb,var(--blue) 72%,var(--ink))}.se-blind .ef-scenario{color:var(--muted)}.se-warn{font-family:var(--mono);font-size:10px;color:var(--orange)}.ef-insession{color:var(--blue);font-size:10px;flex:none}.se-divider{display:flex;align-items:center;gap:8px;padding:5px 10px 3px;font-family:var(--mono);font-size:9px;letter-spacing:.8px;text-transform:uppercase;color:var(--muted)}.se-divider:before,.se-divider:after{content:"";flex:1;border-top:1px dashed var(--line)}.fv-filter{font-family:var(--mono);font-size:11px;line-height:1.35;color:var(--ink2);background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:3px 8px;cursor:pointer;transition:border-color .12s}.fv-filter:hover{border-color:color-mix(in srgb,var(--blue) 45%,var(--line))}.loading{display:flex;align-items:center;justify-content:center;gap:10px;height:100%;flex:1;color:var(--muted);font-family:var(--mono);font-size:13px}.loading.load-error{color:var(--red)}.load-retry{font-family:var(--mono);font-size:12px;color:var(--ink2);background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:3px 10px;cursor:pointer}.load-retry:hover{border-color:var(--muted)}.chrono-tools{display:flex;justify-content:flex-end;margin:0 0 6px}.chrono-all{font-family:var(--mono);font-size:10px;color:var(--muted);background:none;border:1px solid var(--line);border-radius:4px;padding:2px 8px;cursor:pointer}.chrono-all:hover{color:var(--ink2);border-color:var(--muted)}.pane-filter{position:sticky;top:-16px;z-index:2;margin:-8px 0 8px;padding:6px 0;background:var(--panel)}.pane-filter-input{width:100%;box-sizing:border-box;background:var(--panel2);border:1px solid var(--line);border-radius:4px;outline:none;color:var(--ink2);font-family:var(--mono);font-size:11px;padding:4px 8px}.pane-filter-input:focus{border-color:var(--muted)}.pane-filter-input::placeholder{color:var(--muted)}.pane-filter-none{color:var(--muted);font-family:var(--mono);font-size:11px;padding:6px 2px}.ui-tip{position:fixed;z-index:100;pointer-events:none;background:var(--panel2);color:var(--ink2);border:1px solid var(--line);border-radius:6px;padding:4px 9px;font-family:var(--mono);font-size:11px;line-height:1.5;max-width:320px;white-space:pre-line;overflow-wrap:break-word;box-shadow:0 4px 16px #00000038;opacity:0;transition:opacity .14s ease,transform .14s ease}.ui-tip[data-place=top]{transform:translateY(3px)}.ui-tip[data-place=bottom]{transform:translateY(-3px)}.ui-tip.show{opacity:1;transform:translateY(0)}.ui-tip-arrow{position:absolute;width:8px;height:8px;background:var(--panel2);border-right:1px solid var(--line);border-bottom:1px solid var(--line)}.ui-tip[data-place=top] .ui-tip-arrow{bottom:-4.5px;transform:rotate(45deg)}.ui-tip[data-place=bottom] .ui-tip-arrow{top:-4.5px;transform:rotate(225deg)}
|
|
@@ -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-BdRQfrkR.js"></script>
|
|
23
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DEc5Ru3l.css">
|
|
24
24
|
</head>
|
|
25
25
|
<body>
|
|
26
26
|
<div id="root"></div>
|
package/spec-yatsu/src/cli.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'node:fs'
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
2
2
|
import { join, relative, dirname } from 'node:path'
|
|
3
3
|
import { repoRoot, headSha, driftIndex, stagedFiles, git } from '../../spec-cli/src/git.js'
|
|
4
4
|
import { loadSpecs } from '../../spec-cli/src/specs.js'
|
|
5
|
-
import { loadConfig } from '../../spec-cli/src/lint.js'
|
|
5
|
+
import { loadConfig, sourceExtRe } from '../../spec-cli/src/lint.js'
|
|
6
6
|
import { mainBranch, envSessionId, readRawRecord } from '../../spec-cli/src/layout.js'
|
|
7
7
|
import { yatsuNodes, validateScenarios, YATSU_FILE, type YatsuNode } from './yatsu.js'
|
|
8
|
-
import { readReadings, readSidecar, appendReading, appendRetraction, latestPerScenario, evidenceOf, type Reading, type Verdict, type Evidence, type Retraction } from './sidecar.js'
|
|
8
|
+
import { readReadings, readSidecar, appendReading, appendRetraction, latestPerScenario, evidenceOf, isJsonBlob, type Reading, type Verdict, type Evidence, type EvidenceKind, type Retraction } from './sidecar.js'
|
|
9
9
|
import { staleAxes } from './freshness.js'
|
|
10
10
|
import { scenarioIndex } from './scenariofresh.js'
|
|
11
11
|
import { loadEvalRemarkTracks, trackKey } from '../../spec-cli/src/issues.js'
|
|
12
|
+
import { stripRefSigil } from '../../spec-cli/src/mentions.js'
|
|
12
13
|
import { evaluatorTag } from './evaluator.js'
|
|
13
|
-
import { putBlob, listBlobs, gc, isStrayBlob } from './cache.js'
|
|
14
|
-
import { validateTimeline } from './timeline.js'
|
|
15
|
-
import { evalTimeline, type EvalTimeline } from './evaltab.js'
|
|
14
|
+
import { putBlob, blobPath, listBlobs, gc, isStrayBlob } from './cache.js'
|
|
15
|
+
import { validateTimeline, normalizeTimeline } from './timeline.js'
|
|
16
|
+
import { evalTimeline, readBlobByHash, type EvalTimeline } from './evaltab.js'
|
|
16
17
|
|
|
17
18
|
function flag(args: string[], name: string): string | undefined {
|
|
18
19
|
const i = args.indexOf(`--${name}`)
|
|
@@ -52,6 +53,10 @@ function currentNodeId(root: string): string | null {
|
|
|
52
53
|
return null
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
// isUiPath answers a FRONTEND-specific question — "does this node need a real BROWSER reading?" — and is
|
|
57
|
+
// consumed by the review-proof's `uncoveredFrontend` blindspot, NOT by scan's uncovered check. Scan keys off
|
|
58
|
+
// the general `sourceExtensions` knob instead (see the `yatsu-uncovered` branch below), so a non-web project's
|
|
59
|
+
// sources are held to the loss discipline too; this stays web-shaped on purpose.
|
|
55
60
|
const UI_FILE = /\.(jsx|tsx|vue|svelte|css)$/
|
|
56
61
|
export const isUiPath = (p: string) => UI_FILE.test(p) || p.includes('spec-dashboard/')
|
|
57
62
|
|
|
@@ -83,6 +88,9 @@ export function nodeChanged(dirRel: string, codeFiles: string[], changed: Set<st
|
|
|
83
88
|
async function scan(args: string[] = []): Promise<number> {
|
|
84
89
|
const root = repoRoot()
|
|
85
90
|
const cfg = loadConfig(root)
|
|
91
|
+
// yatsu-uncovered fires on ANY governed source file (per the shared `sourceExtensions` knob), not just
|
|
92
|
+
// frontend — so a backend/CLI/non-web project's sources are held to the loss discipline too, not exempted.
|
|
93
|
+
const srcRe = sourceExtRe(cfg.sourceExtensions)
|
|
86
94
|
const changedOnly = has(args, 'changed')
|
|
87
95
|
const changed = changedOnly ? changedSinceBase(root) : null
|
|
88
96
|
const idx = await driftIndex(root)
|
|
@@ -152,9 +160,9 @@ async function scan(args: string[] = []): Promise<number> {
|
|
|
152
160
|
const names = orphans.map((o) => `'${o.scenario}' (${o.threadId}, ${o.remarks.length} remark${o.remarks.length > 1 ? 's' : ''})`).join(', ')
|
|
153
161
|
findings.push(` • yatsu-dangling: '${s.id}' has ${orphans.length} orphaned remark track(s) — scenario ${names} renamed/deleted; resolve/retract via \`spex resolve <ref>\` / \`spex retract <ref>\` or restore the scenario name`)
|
|
154
162
|
}
|
|
155
|
-
} else if (s.code.some(
|
|
163
|
+
} else if (s.code.some((p) => srcRe.test(p))) {
|
|
156
164
|
uncovered++
|
|
157
|
-
findings.push(` • yatsu-uncovered: '${s.id}' governs
|
|
165
|
+
findings.push(` • yatsu-uncovered: '${s.id}' governs source code but has no yatsu.md — give it a scenario (description + expected) so its loss can be measured`)
|
|
158
166
|
}
|
|
159
167
|
if (!findings.length) continue
|
|
160
168
|
flaggedNodes++
|
|
@@ -185,6 +193,13 @@ async function scan(args: string[] = []): Promise<number> {
|
|
|
185
193
|
// `--image`, and a misfiled reading is worse than none (it reads as proof). Value-flags consume the next
|
|
186
194
|
// token, so a path/note that itself starts with `--` is never mistaken for a flag.
|
|
187
195
|
const EVAL_VALUE_FLAGS = new Set(['scenario', 'note', 'image', 'result', 'video', 'timeline'])
|
|
196
|
+
|
|
197
|
+
// which axis each evidence kind carries — a step-map ([[step-timeline]]) anchors to ONE of them, so its
|
|
198
|
+
// `axis` must match a present entry's kind. A video is time (ms), a transcript is line-numbered, a still
|
|
199
|
+
// SEQUENCE is frame-indexed, and structured `data`'s positions are record ordinals — the `index` axis (a
|
|
200
|
+
// bare ordinal). Render kind and step-map axis stay ORTHOGONAL ([[evidence-kind-taxonomy]]): this map is
|
|
201
|
+
// the one seam between them, one row per kind.
|
|
202
|
+
const AXIS_FOR_KIND: Record<EvidenceKind, string> = { video: 'time', transcript: 'line', image: 'frame', data: 'index' }
|
|
188
203
|
const EVAL_BOOL_FLAGS = new Set(['pass', 'fail'])
|
|
189
204
|
function rejectUnknownEvalFlag(args: string[]): string | null {
|
|
190
205
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -206,7 +221,7 @@ async function evalCmd(args: string[]): Promise<number> {
|
|
|
206
221
|
return 2
|
|
207
222
|
}
|
|
208
223
|
const sel = positional(args)
|
|
209
|
-
const id = !sel || sel === '.' ? currentNodeId(root) : sel
|
|
224
|
+
const id = !sel || sel === '.' ? currentNodeId(root) : stripRefSigil(sel) // node args tolerate @/[[ ]] sigils ([[mentions]])
|
|
210
225
|
if (!id) { console.error('spex yatsu eval .: no current node (no .session/node-branch here) — name a node'); return 2 }
|
|
211
226
|
const node = (await gatherNodes(root)).find((n) => n.id === id)
|
|
212
227
|
if (!node) { console.error(`spex yatsu eval: no yatsu node '${id}' (a node needs a yatsu.md)`); return 1 }
|
|
@@ -235,16 +250,23 @@ async function evalCmd(args: string[]): Promise<number> {
|
|
|
235
250
|
const video = flag(args, 'video')
|
|
236
251
|
const evidence: Evidence[] = []
|
|
237
252
|
for (const p of images) evidence.push({ hash: putBlob(readFileSync(p)), kind: 'image' })
|
|
238
|
-
if (result !== undefined)
|
|
253
|
+
if (result !== undefined) {
|
|
254
|
+
// --result's kind is DERIVED FROM CONTENT ([[evidence-kind-taxonomy]]), not welded to the flag: a
|
|
255
|
+
// structured export (JSON) files as `data` (a validatable data block), free-form output as `transcript`.
|
|
256
|
+
const bytes = readFileSync(result === '-' ? 0 : result)
|
|
257
|
+
evidence.push({ hash: putBlob(bytes), kind: isJsonBlob(bytes) ? 'data' : 'transcript' })
|
|
258
|
+
}
|
|
239
259
|
if (video !== undefined) evidence.push({ hash: putBlob(readFileSync(video)), kind: 'video' })
|
|
240
260
|
|
|
241
|
-
// --timeline: the
|
|
242
|
-
//
|
|
243
|
-
//
|
|
261
|
+
// --timeline: the evidence's step map (timeline.ts) — steps anchored to a POSITION on the evidence's OWN
|
|
262
|
+
// axis, no longer video-welded. It accompanies ANY axis-bearing evidence, and its `axis` must MATCH a
|
|
263
|
+
// present entry's kind (a video is time, a transcript is line, a still sequence is frame — AXIS_FOR_KIND):
|
|
264
|
+
// a step-map for an axis nothing here carries is misfiled. Validated LOUD at filing (a malformed map is
|
|
265
|
+
// rejected, never silently reshaped); stored canonicalized so identical timelines share one blob.
|
|
244
266
|
const timeline = flag(args, 'timeline')
|
|
245
267
|
let timelineBlob: string | undefined
|
|
246
268
|
if (timeline !== undefined) {
|
|
247
|
-
if (!evidence.
|
|
269
|
+
if (!evidence.length) { console.error('spex yatsu eval: --timeline needs axis-bearing evidence — attach the --video/--image/--result whose axis its steps anchor to'); return 2 }
|
|
248
270
|
let parsed: unknown
|
|
249
271
|
try { parsed = JSON.parse(readFileSync(timeline, 'utf8')) } catch { console.error(`spex yatsu eval: --timeline ${timeline} is not readable JSON`); return 2 }
|
|
250
272
|
const terrs = validateTimeline(parsed)
|
|
@@ -253,6 +275,13 @@ async function evalCmd(args: string[]): Promise<number> {
|
|
|
253
275
|
for (const e of terrs) console.error(` ${e}`)
|
|
254
276
|
return 2
|
|
255
277
|
}
|
|
278
|
+
const { axis } = normalizeTimeline(parsed)
|
|
279
|
+
const present = new Set(evidence.map((e) => AXIS_FOR_KIND[e.kind]))
|
|
280
|
+
if (!present.has(axis)) {
|
|
281
|
+
const have = evidence.map((e) => `${e.kind}→${AXIS_FOR_KIND[e.kind]}`).join(', ')
|
|
282
|
+
console.error(`spex yatsu eval: --timeline axis '${axis}' matches none of this reading's evidence (${have}) — a step-map's axis must be the axis of an attached evidence entry`)
|
|
283
|
+
return 2
|
|
284
|
+
}
|
|
256
285
|
timelineBlob = putBlob(Buffer.from(JSON.stringify(parsed)))
|
|
257
286
|
}
|
|
258
287
|
|
|
@@ -273,9 +302,34 @@ async function evalCmd(args: string[]): Promise<number> {
|
|
|
273
302
|
: 'no evidence'
|
|
274
303
|
console.log(` ✓ '${id}' scenario '${scenario.name}' → ${verdictText(verdict)} @ ${reading.codeSha.slice(0, 7)} [${reading.evaluator}] (${ev})`)
|
|
275
304
|
console.log(`spex yatsu eval: 1 measurement filed`)
|
|
305
|
+
|
|
306
|
+
// @@@mis-anchor guard - a codeSha names a COMMIT, never a working tree: filed over uncommitted governed
|
|
307
|
+
// edits, this reading claims a verdict at HEAD while HEAD lacks the code actually measured — wrong from
|
|
308
|
+
// birth, and the stale flag after the next commit is freshness exposing it, not an engine bug. Warn,
|
|
309
|
+
// never block: the honest flow is measure on the tree until green → commit that tested tree → file.
|
|
310
|
+
const dirty = dirtyGoverned(root, [...(scenario.code?.length ? scenario.code : node.codeFiles), node.yatsuPath])
|
|
311
|
+
if (dirty.length) {
|
|
312
|
+
console.error(` ⚠ mis-anchored reading: uncommitted changes in governed ${dirty.join(', ')}`)
|
|
313
|
+
console.error(` this reading anchors to HEAD ${reading.codeSha.slice(0, 7)}, which does NOT contain those edits — it claims a ${verdict.status} for code that never ran, and the commit that lands them will (correctly) read it stale.`)
|
|
314
|
+
console.error(` the honest flow: measure on the tree until green → commit that just-tested tree (code+spec) → THEN file the reading against the clean HEAD (retract this one if it recorded the dirty run).`)
|
|
315
|
+
}
|
|
276
316
|
return 0
|
|
277
317
|
}
|
|
278
318
|
|
|
319
|
+
// the governed paths with uncommitted changes (staged, unstaged, or untracked) — the mis-anchor guard's
|
|
320
|
+
// probe. Paths are repo-relative pathspecs, so a governed dir prefix or `*` glob scopes the same way
|
|
321
|
+
// nodeChanged matches it; a rename line reports its new name.
|
|
322
|
+
function dirtyGoverned(root: string, paths: string[]): string[] {
|
|
323
|
+
if (!paths.length) return []
|
|
324
|
+
const out = git(['-C', root, 'status', '--porcelain', '--', ...paths])
|
|
325
|
+
const files = out.split('\n').filter(Boolean).map((l) => {
|
|
326
|
+
const p = l.slice(3)
|
|
327
|
+
const i = p.indexOf(' -> ')
|
|
328
|
+
return i >= 0 ? p.slice(i + 4) : p
|
|
329
|
+
})
|
|
330
|
+
return [...new Set(files)]
|
|
331
|
+
}
|
|
332
|
+
|
|
279
333
|
// the verdict from the flags: --pass or --fail sets the status (pass wins if both given); --note <text> is an
|
|
280
334
|
// OPTIONAL annotation attached to either. No status flag → null (a measurement must commit to pass or fail).
|
|
281
335
|
function parseVerdict(args: string[]): Verdict | null {
|
|
@@ -309,7 +363,7 @@ async function retractCmd(args: string[]): Promise<number> {
|
|
|
309
363
|
return 2
|
|
310
364
|
}
|
|
311
365
|
const sel = positional(args)
|
|
312
|
-
const id = !sel || sel === '.' ? currentNodeId(root) : sel
|
|
366
|
+
const id = !sel || sel === '.' ? currentNodeId(root) : stripRefSigil(sel)
|
|
313
367
|
if (!id) { console.error('spex yatsu retract .: no current node (no .session/node-branch here) — name a node'); return 2 }
|
|
314
368
|
const node = yatsuNodes(root).find((n) => n.id === id)
|
|
315
369
|
if (!node) { console.error(`spex yatsu retract: no yatsu node '${id}' (a node needs a yatsu.md)`); return 1 }
|
|
@@ -410,7 +464,7 @@ function checkStaged(): number {
|
|
|
410
464
|
async function show(args: string[]): Promise<number> {
|
|
411
465
|
const root = repoRoot()
|
|
412
466
|
const sel = positional(args)
|
|
413
|
-
const id = !sel || sel === '.' ? currentNodeId(root) : sel
|
|
467
|
+
const id = !sel || sel === '.' ? currentNodeId(root) : stripRefSigil(sel)
|
|
414
468
|
if (!id) { console.error('spex yatsu show .: no current node (no .session/node-branch here) — name a node'); return 2 }
|
|
415
469
|
const tl = await evalTimeline(id) // no ctx → evalTimeline derives specs + driftIndex itself for this one id
|
|
416
470
|
if (has(args, 'json')) { console.log(JSON.stringify(tl, null, 2)); return 0 }
|
|
@@ -460,20 +514,24 @@ export async function runYatsu(args: string[]): Promise<number> {
|
|
|
460
514
|
if (sub === 'clean') return clean(args.slice(1))
|
|
461
515
|
if (sub === 'show') return show(args.slice(1))
|
|
462
516
|
if (sub === 'check-staged') return checkStaged()
|
|
463
|
-
console.error('spex yatsu: scan [--changed] | eval [.|<node>] [--scenario <name>] (--pass|--fail) [--note <text>] [--image <path> …repeatable] [--result <path|->] [--video <path> [--timeline <json>]
|
|
517
|
+
console.error('spex yatsu: scan [--changed] | eval [.|<node>] [--scenario <name>] (--pass|--fail) [--note <text>] [--image <path> …repeatable] [--result <path|->] [--video <path>] [--timeline <json>] | retract [.|<node>] [--scenario <name>] [--last | --ts <iso>] [--note <why>] | show [.|<node>] [--json] | clean [--keep-latest|--all]')
|
|
464
518
|
return 2
|
|
465
519
|
}
|
|
466
520
|
|
|
467
|
-
// `spex blob put <file|->` ([[blob-put]]) — the bare
|
|
468
|
-
//
|
|
469
|
-
//
|
|
521
|
+
// `spex blob put <file|->` / `spex blob get <hash> [-o <file>]` ([[blob-put]], [[blob-get]]) — the bare
|
|
522
|
+
// evidence-transport pair: put stashes bytes in the shared content-addressed cache and prints the hash,
|
|
523
|
+
// WITHOUT filing a reading (`yatsu eval --video` couples the two); get is its symmetric read — hash in,
|
|
524
|
+
// bytes out. putBlob is idempotent by content, so re-putting re-seeds a checkout whose cache lacks a blob
|
|
470
525
|
// some thread already references by hash (the clone-evidence-404 repair).
|
|
471
|
-
export function runBlob(args: string[]): number {
|
|
472
|
-
|
|
473
|
-
if (args[0]
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
526
|
+
export async function runBlob(args: string[]): Promise<number> {
|
|
527
|
+
if (args[0] === 'put' && args[1] !== undefined) return blobPut(args[1])
|
|
528
|
+
if (args[0] === 'get') return blobGet(args.slice(1))
|
|
529
|
+
console.error('spex blob: put <file|-> — stash bytes in the shared evidence cache, print the content hash')
|
|
530
|
+
console.error(' get <hash> [-o <file>] — read a blob back: local cache first, backend fallback')
|
|
531
|
+
return 2
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function blobPut(file: string): number {
|
|
477
535
|
let bytes: Buffer
|
|
478
536
|
try { bytes = readFileSync(file === '-' ? 0 : file) } catch (e) {
|
|
479
537
|
console.error(`spex blob put: cannot read ${file}: ${(e as Error).message}`)
|
|
@@ -483,3 +541,47 @@ export function runBlob(args: string[]): number {
|
|
|
483
541
|
console.log(putBlob(bytes))
|
|
484
542
|
return 0
|
|
485
543
|
}
|
|
544
|
+
|
|
545
|
+
// the read half: ① the local content-addressed cache (the evidence usually IS on this disk — no backend
|
|
546
|
+
// needed), ② on a local miss the same GET /api/yatsu/blob/:hash the dashboard streams from (the blob may
|
|
547
|
+
// have been pruned here, or put on another machine sharing the backend), ③ both missed → fail loud naming
|
|
548
|
+
// both paths. No third read mechanism — this reuses readBlobByHash and the existing endpoint verbatim.
|
|
549
|
+
async function blobGet(args: string[]): Promise<number> {
|
|
550
|
+
const oIdx = args.indexOf('-o')
|
|
551
|
+
const out = oIdx >= 0 ? args[oIdx + 1] : undefined
|
|
552
|
+
if (oIdx >= 0 && (out === undefined || out.startsWith('-'))) { console.error('spex blob get: -o needs a <file>'); return 2 }
|
|
553
|
+
const hash = args.find((a, i) => (oIdx < 0 || (i !== oIdx && i !== oIdx + 1)) && !a.startsWith('-'))
|
|
554
|
+
if (!hash) { console.error('spex blob get: usage: spex blob get <hash> [-o <file>]'); return 2 }
|
|
555
|
+
const local = readBlobByHash(hash) // validates 64-hex before touching the fs, then reads the shared cache
|
|
556
|
+
if (local.ok) return emitBlob(local.bytes, out)
|
|
557
|
+
if (local.reason === 'invalid') { console.error(`spex blob get: bad hash '${hash}' — a blob hash is 64 hex chars`); return 2 }
|
|
558
|
+
const { apiBase } = await import('../../spec-cli/src/sessions.js')
|
|
559
|
+
const url = `${await apiBase()}/api/yatsu/blob/${hash}`
|
|
560
|
+
let backendMiss: string
|
|
561
|
+
try {
|
|
562
|
+
const r = await fetch(url)
|
|
563
|
+
if (r.ok) return emitBlob(Buffer.from(await r.arrayBuffer()), out)
|
|
564
|
+
backendMiss = `HTTP ${r.status}`
|
|
565
|
+
} catch (e) {
|
|
566
|
+
backendMiss = `unreachable (${(e as Error).message})`
|
|
567
|
+
}
|
|
568
|
+
console.error(`spex blob get: ${hash} — not found on either path:`)
|
|
569
|
+
console.error(` local cache: ${blobPath(hash)} — no such blob (pruned, or put on another machine)`)
|
|
570
|
+
console.error(` backend: ${url} — ${backendMiss}`)
|
|
571
|
+
return 1
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// default stdout (pipe-friendly; the cli.ts flushExit drains a large piped dump before exit), -o writes a
|
|
575
|
+
// file. Raw bytes straight at a human's terminal get a one-line stderr warning, not a block.
|
|
576
|
+
function emitBlob(bytes: Buffer, out?: string): number {
|
|
577
|
+
if (out !== undefined) {
|
|
578
|
+
try { writeFileSync(out, bytes) } catch (e) {
|
|
579
|
+
console.error(`spex blob get: cannot write ${out}: ${(e as Error).message}`)
|
|
580
|
+
return 2
|
|
581
|
+
}
|
|
582
|
+
return 0
|
|
583
|
+
}
|
|
584
|
+
if (process.stdout.isTTY) console.error(`spex blob get: writing ${bytes.length} raw bytes to a tty — pipe it or use -o <file>`)
|
|
585
|
+
process.stdout.write(bytes)
|
|
586
|
+
return 0
|
|
587
|
+
}
|
|
@@ -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 { yatsuNodes, type YatsuNode } from './yatsu.js'
|
|
6
|
-
import { readSidecar, applyRetractions, evidenceOf, type Verdict, type EvidenceKind, type Retraction } from './sidecar.js'
|
|
6
|
+
import { readSidecar, applyRetractions, evidenceOf, isJsonBlob, type Verdict, type EvidenceKind, type Retraction } from './sidecar.js'
|
|
7
7
|
import { staleAxes, codeDrift, type StaleAxis } from './freshness.js'
|
|
8
8
|
import { scenarioIndex, type ScenarioIndex } from './scenariofresh.js'
|
|
9
9
|
import { hasBlob, getBlob, MISS_BLOB } from './cache.js'
|
|
@@ -245,10 +245,11 @@ export function readBlobByHash(hash: string, dir?: string): BlobResult {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
// PNG/JPEG/GIF/WebP cover every screenshot (a manual --image); MP4/WebM cover a recorded clip (--video), so
|
|
248
|
-
// the blob route serves it with a playable Content-Type
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
248
|
+
// the blob route serves it with a playable Content-Type. Text bytes (no NUL, no known header) split by
|
|
249
|
+
// CONTENT ([[evidence-kind-taxonomy]]): a structured export (JSON) sniffs to application/json — so the
|
|
250
|
+
// `data` renderer knows to validate/pretty-print it — while free-form terminal text stays text/plain;
|
|
251
|
+
// anything else falls back to a generic binary type so it still downloads rather than being mislabeled.
|
|
252
|
+
export function sniffBlobMime(b: Buffer): string {
|
|
252
253
|
if (b.length >= 4 && b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) return 'image/png'
|
|
253
254
|
if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return 'image/jpeg'
|
|
254
255
|
if (b.length >= 4 && b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46) return 'image/gif'
|
|
@@ -257,6 +258,6 @@ function sniffBlobMime(b: Buffer): string {
|
|
|
257
258
|
// ISO-BMFF (MP4/MOV): a `ftyp` box type at bytes 4..8, after its 4-byte size.
|
|
258
259
|
if (b.length >= 12 && b.toString('ascii', 4, 8) === 'ftyp') return 'video/mp4'
|
|
259
260
|
if (b.length >= 12 && b.toString('ascii', 0, 4) === 'RIFF' && b.toString('ascii', 8, 12) === 'WEBP') return 'image/webp'
|
|
260
|
-
if (b.length && !b.includes(0)) return 'text/plain; charset=utf-8'
|
|
261
|
+
if (b.length && !b.includes(0)) return isJsonBlob(b) ? 'application/json' : 'text/plain; charset=utf-8'
|
|
261
262
|
return 'application/octet-stream'
|
|
262
263
|
}
|
package/spec-yatsu/src/filing.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { repoRoot, headSha } from '../../spec-cli/src/git.js'
|
|
2
2
|
import { yatsuNodes } from './yatsu.js'
|
|
3
|
-
import { appendReading, readReadings, type Reading } from './sidecar.js'
|
|
3
|
+
import { appendReading, readReadings, isJsonBlob, type Reading, type EvidenceKind } from './sidecar.js'
|
|
4
4
|
import { putBlob } from './cache.js'
|
|
5
5
|
|
|
6
6
|
export type FileResult = { ok: true; reading: Reading } | { ok: false; error: string }
|
|
@@ -20,11 +20,14 @@ export function fileHumanReading(
|
|
|
20
20
|
const sc = node.scenarios.find((s) => s.name === input.scenario)
|
|
21
21
|
if (!sc) return { ok: false, error: `'${nodeId}' has no scenario '${input.scenario}'` }
|
|
22
22
|
if (input.status !== 'pass' && input.status !== 'fail') return { ok: false, error: 'status must be pass or fail' }
|
|
23
|
-
|
|
23
|
+
// the evidence bytes; its kind is derived from CONTENT ([[evidence-kind-taxonomy]]) — a structured JSON
|
|
24
|
+
// export files as `data`, free-form text as `transcript` — so the HTTP filer agrees with the CLI.
|
|
25
|
+
const buf = input.transcript ? Buffer.from(input.transcript) : null
|
|
26
|
+
const blob = buf ? putBlob(buf) : null
|
|
24
27
|
const reading: Reading = {
|
|
25
28
|
scenario: sc.name,
|
|
26
29
|
codeSha: headSha(root),
|
|
27
|
-
...(blob ? { evidence: [{ hash: blob, kind: 'transcript' as
|
|
30
|
+
...(blob ? { evidence: [{ hash: blob, kind: (buf && isJsonBlob(buf) ? 'data' : 'transcript') as EvidenceKind }] } : {}),
|
|
28
31
|
evaluator: 'manual@1',
|
|
29
32
|
// the filing session (caller-passed — the human annotator has no reachable session, so it stays absent
|
|
30
33
|
// there and the eval-comment loop-in is silent, per [[mentions]])
|
package/spec-yatsu/src/proof.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type ProofReading = {
|
|
|
25
25
|
| { kind: 'image'; dataUri: string }
|
|
26
26
|
| { kind: 'video'; dataUri: string }
|
|
27
27
|
| { kind: 'transcript'; text: string }
|
|
28
|
+
| { kind: 'data'; text: string }
|
|
28
29
|
| { kind: 'miss' }
|
|
29
30
|
| { kind: 'none' }
|
|
30
31
|
}
|
|
@@ -200,6 +201,13 @@ async function toProofReading(r: EvalEntry): Promise<ProofReading> {
|
|
|
200
201
|
if (!blob.ok) return { ...base, evidence: { kind: 'miss' } }
|
|
201
202
|
if (blob.mime.startsWith('image/')) return { ...base, evidence: { kind: 'image', dataUri: `data:${blob.mime};base64,${blob.bytes.toString('base64')}` } }
|
|
202
203
|
if (blob.mime.startsWith('video/')) return { ...base, evidence: { kind: 'video', dataUri: `data:${blob.mime};base64,${blob.bytes.toString('base64')}` } }
|
|
204
|
+
// structured data ([[evidence-kind-taxonomy]]): pretty-print so the self-contained proof shows a
|
|
205
|
+
// validatable data block, not one flat line; invalid JSON falls back to the raw bytes rather than hiding.
|
|
206
|
+
if (blob.mime.startsWith('application/json')) {
|
|
207
|
+
let text = blob.bytes.toString('utf8')
|
|
208
|
+
try { text = JSON.stringify(JSON.parse(text), null, 2) } catch { /* keep raw — invalid JSON still shows */ }
|
|
209
|
+
return { ...base, evidence: { kind: 'data', text } }
|
|
210
|
+
}
|
|
203
211
|
return { ...base, evidence: { kind: 'transcript', text: blob.bytes.toString('utf8') } }
|
|
204
212
|
}
|
|
205
213
|
|
|
@@ -310,6 +318,7 @@ function renderReading(r: ProofReading): string {
|
|
|
310
318
|
ev.kind === 'image' ? `<img class="shot" src="${ev.dataUri}" alt="${esc(r.scenario)}">`
|
|
311
319
|
: ev.kind === 'video' ? `<video class="shot" src="${ev.dataUri}" controls preload="metadata"></video>`
|
|
312
320
|
: ev.kind === 'transcript' ? `<pre class="transcript">${esc(ev.text)}</pre>`
|
|
321
|
+
: ev.kind === 'data' ? `<pre class="transcript data">${esc(ev.text)}</pre>`
|
|
313
322
|
: ev.kind === 'miss' ? `<div class="noev">⌀ miss original file — the evidence bytes were pruned</div>`
|
|
314
323
|
: `<div class="noev">attested without a capture</div>`
|
|
315
324
|
const stale = r.fresh ? '' : `<span class="stale" title="${esc(r.staleAxes.join(', '))} changed since the reading">stale</span>`
|
|
@@ -483,6 +492,7 @@ h2{margin:42px 0 16px;font-size:14px;letter-spacing:.14em;text-transform:upperca
|
|
|
483
492
|
.evidence{margin:12px 0 0}
|
|
484
493
|
.shot{max-width:100%;border:1px solid var(--line);border-radius:8px;box-shadow:0 14px 40px -24px #000;display:block}
|
|
485
494
|
.transcript{max-height:420px;overflow:auto;margin:0;padding:12px 14px;background:#05080d;border:1px solid var(--line);border-radius:8px;font-size:12px;color:#9fdcc6;white-space:pre-wrap;word-break:break-word}
|
|
495
|
+
.transcript.data{color:#cbb6ff;border-left:3px solid #6b46c1}
|
|
486
496
|
.noev,.blindspot{margin-top:10px;padding:10px 12px;border-radius:8px;font-size:13px;color:var(--dim);background:#0a0e15;border:1px dashed var(--line)}
|
|
487
497
|
.blindspot{color:var(--amber);border-color:#3a3320}
|
|
488
498
|
.gates{list-style:none;margin:0;padding:0;border:1px solid var(--line);border-radius:12px;overflow:hidden}
|