codehere 0.8.0 → 1.0.1
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 +22 -13
- package/dist/application/services/verdict-service.js +154 -0
- package/dist/application/use-cases/assess-cross-vendor.js +126 -0
- package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
- package/dist/application/use-cases/export-aat.js +107 -0
- package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
- package/dist/application/use-cases/generate-final-report.js +140 -15
- package/dist/application/use-cases/generate-narration.js +5 -3
- package/dist/application/use-cases/generate-thread-title.js +5 -2
- package/dist/application/use-cases/plan-auto-review.js +57 -0
- package/dist/application/use-cases/reconcile-audit.js +100 -0
- package/dist/application/use-cases/rerun-tests.js +226 -0
- package/dist/application/use-cases/search-sessions.js +70 -0
- package/dist/application/use-cases/should-auto-review.js +71 -0
- package/dist/application/use-cases/thread-export.js +90 -0
- package/dist/application/use-cases/verify-claim.js +187 -5
- package/dist/domain/audit.js +11 -0
- package/dist/domain/services/codebase-detector.js +0 -6
- package/dist/domain/task.js +91 -0
- package/dist/error-handler.js +3 -115
- package/dist/index.js +269 -25
- package/dist/infrastructure/completion/bash-completion.js +7 -3
- package/dist/infrastructure/completion/fish-completion.js +7 -12
- package/dist/infrastructure/completion/zsh-completion.js +7 -12
- package/dist/infrastructure/config/config-manager.js +4 -3
- package/dist/infrastructure/config/product-context.js +1 -19
- package/dist/infrastructure/context/adapters/figma-source.js +37 -2
- package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
- package/dist/infrastructure/context/github-api.js +110 -0
- package/dist/infrastructure/context/github-device-flow.js +65 -0
- package/dist/infrastructure/context/oauth-store.js +5 -2
- package/dist/infrastructure/context/registry-bootstrap.js +0 -4
- package/dist/infrastructure/cost/budget-config.js +3 -2
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +90 -15
- package/dist/infrastructure/cost/team-ledger.js +355 -0
- package/dist/infrastructure/discovery/discovery-log.js +85 -0
- package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +133 -3
- package/dist/infrastructure/hooks/hook-mode.js +20 -4
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
- package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
- package/dist/infrastructure/observability/execution-tracer.js +1 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +22 -10
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
- package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
- package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
- package/dist/infrastructure/orchestration/context-handoff.js +32 -14
- package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
- package/dist/infrastructure/orchestration/failover-config.js +0 -10
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +132 -1
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +164 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +238 -21
- package/dist/infrastructure/orchestration/session-manager.js +593 -94
- package/dist/infrastructure/orchestration/session-persistence.js +177 -41
- package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1462 -380
- package/dist/infrastructure/protocols/mcp-tools.js +401 -0
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
- package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
- package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
- package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
- package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
- package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
- package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
- package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
- package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
- package/dist/infrastructure/protocols/ui/styles.js +2147 -0
- package/dist/infrastructure/protocols/ui/tokens.js +96 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +116 -4214
- package/dist/infrastructure/protocols/verify-page.js +849 -155
- package/dist/infrastructure/security/fix-code.js +144 -0
- package/dist/infrastructure/security/fix-secret.js +89 -0
- package/dist/infrastructure/security/permissions-config.js +3 -2
- package/dist/infrastructure/security/scan-types.js +10 -0
- package/dist/infrastructure/security/ship-scan.js +195 -0
- package/dist/infrastructure/security/trust-scorer.js +70 -3
- package/dist/infrastructure/security/vibe-checks.js +99 -0
- package/dist/infrastructure/storage/atomic-write.js +86 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +150 -3
- package/dist/infrastructure/storage/project-thread-reader.js +261 -24
- package/dist/monitoring.js +2 -2
- package/dist/presentation/cli/commands/audit-command.js +79 -4
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/check-command.js +96 -0
- package/dist/presentation/cli/commands/context-source-command.js +20 -1
- package/dist/presentation/cli/commands/help-command.js +5 -5
- package/dist/presentation/cli/commands/hook-command.js +5 -5
- package/dist/presentation/cli/commands/init-command.js +26 -14
- package/dist/presentation/cli/commands/memory-command.js +28 -28
- package/dist/presentation/cli/commands/permission-command.js +1 -12
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/protocols-command.js +4 -4
- package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
- package/dist/presentation/cli/commands/report-command.js +48 -32
- package/dist/presentation/cli/commands/scan-command.js +52 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
- package/dist/presentation/cli/commands/search-command.js +51 -0
- package/dist/presentation/cli/commands/setup-command.js +31 -8
- package/dist/presentation/cli/commands/status-command.js +39 -86
- package/dist/presentation/cli/commands/team-command.js +256 -0
- package/dist/presentation/cli/commands/thread-command.js +87 -5
- package/dist/presentation/cli/commands/trust-command.js +53 -31
- package/dist/presentation/cli/commands/verify-command.js +74 -9
- package/dist/presentation/cli/error-display.js +0 -8
- package/dist/presentation/cli/repl.js +15 -7
- package/dist/presentation/cli/task-deliverable.js +36 -10
- package/dist/testing/replay-adapter.js +0 -19
- package/dist/ui.js +51 -239
- package/dist/utils/version.js +0 -6
- package/package.json +30 -10
- package/dist/domain/entities/task.js +0 -61
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/task-repository.js +0 -206
|
@@ -22,122 +22,131 @@
|
|
|
22
22
|
* highlight ring + caption (the visibleAct pattern) and can record one .webm
|
|
23
23
|
* per fixture in-browser, or be driven headless by scripts/record-replay.mjs.
|
|
24
24
|
*/
|
|
25
|
+
import { violatesGitInvariant } from '../../application/use-cases/verify-claim.js';
|
|
26
|
+
import { DESIGN_TOKENS } from './ui/tokens.js';
|
|
27
|
+
import { FONT_FACES } from './ui/font-faces.js';
|
|
25
28
|
export const VERIFY_HTML = `<!doctype html>
|
|
26
29
|
<html lang="en">
|
|
27
30
|
<head>
|
|
28
31
|
<meta charset="utf-8" />
|
|
29
32
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
33
|
+
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
|
30
34
|
<title>codehere · /verify</title>
|
|
31
|
-
<style
|
|
32
|
-
:root
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
--allow:#28c840; --block:#ff5f57; --warn:#febc2e; --info:#76b7ff;
|
|
39
|
-
--radius:8px;
|
|
40
|
-
--sans:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
41
|
-
--mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;
|
|
42
|
-
}
|
|
35
|
+
<style>${FONT_FACES}${DESIGN_TOKENS}
|
|
36
|
+
/* /verify used to declare its own :root here: cool-gray surfaces from the
|
|
37
|
+
pre-v1 palette, Inter as the voice face, and macOS traffic lights for the
|
|
38
|
+
verdict colours (--allow was var(--verified)). That made three different "verified"
|
|
39
|
+
greens in one product, on a page linked directly from codehere start.
|
|
40
|
+
It now takes the same tokens and the same self-hosted faces as the
|
|
41
|
+
dashboard. DESIGN.md §4.5 always said it should. */
|
|
43
42
|
*{box-sizing:border-box}
|
|
44
43
|
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);font-size:13px;line-height:1.55;-webkit-font-smoothing:antialiased;
|
|
45
44
|
background-image:radial-gradient(circle,rgba(74,144,226,.045) 1px,transparent 1px);background-size:24px 24px;}
|
|
46
45
|
a{color:inherit}
|
|
47
46
|
header.top{display:flex;align-items:center;gap:14px;padding:10px 20px;border-bottom:1px solid var(--border-soft);background:rgba(0,0,0,.55);backdrop-filter:blur(8px);position:sticky;top:0;z-index:5;flex-wrap:wrap}
|
|
48
|
-
.logo{font-size:
|
|
49
|
-
|
|
50
|
-
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
47
|
+
.logo{font-size:14.5px;font-weight:700;display:inline-flex;align-items:center;gap:7px;letter-spacing:-.01em}
|
|
48
|
+
/* S16 brand-sync: gradient name only, no dot — the landing nav wordmark carries none */
|
|
51
49
|
.logo .name{background:linear-gradient(135deg,#ff9a76 0%,#4a90e2 100%);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
|
52
50
|
.route{color:var(--fg-40);font-family:var(--mono);font-size:12px}
|
|
53
51
|
.pill{font-size:10.5px;padding:3px 8px;border:1px solid var(--border);border-radius:999px;color:var(--fg-50)}
|
|
54
|
-
.pill.local{border-color:
|
|
52
|
+
.pill.local{border-color:var(--border);color:var(--fg-70)}
|
|
55
53
|
.spacer{flex:1}
|
|
56
|
-
.hint{color:var(--fg-40);font-size:
|
|
54
|
+
.hint{color:var(--fg-40);font-size:12px}
|
|
57
55
|
.hint code{font-family:var(--mono);color:var(--info)}
|
|
58
56
|
.wrap{max-width:1180px;margin:0 auto;padding:22px 20px 80px}
|
|
59
57
|
.actions{display:flex;align-items:center;gap:12px;margin:18px 0 8px;flex-wrap:wrap}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
/* S17 · same token system as the product page: ONE flat accent fill per
|
|
59
|
+
action row (no gradient, no glow), calm hover, visible focus ring.
|
|
60
|
+
Buttons don't inherit type by default — pull every control into the system. */
|
|
61
|
+
button,select,input{font:inherit}
|
|
62
|
+
button.run{background:var(--accent);color:#1a0f08;border:0;font-weight:600;font-size:13px;padding:9px 18px;border-radius:var(--radius);cursor:pointer;transition:filter var(--dur) var(--ease)}
|
|
63
|
+
button.run:hover{filter:brightness(1.06)}
|
|
64
|
+
button.run:disabled{opacity:.6;cursor:default;filter:none}
|
|
65
|
+
button.run:focus-visible,.linkbtn:focus-visible,.cell:focus-visible,.steprow:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
66
|
+
.linkbtn{color:var(--fg-70);text-decoration:none;border:1px solid var(--border);padding:8px 14px;border-radius:var(--radius);font-size:12px;cursor:pointer;background:transparent}
|
|
63
67
|
.linkbtn:hover{border-color:var(--accent);color:var(--fg)}
|
|
64
68
|
.tally{display:inline-flex;gap:14px;align-items:center;font-family:var(--mono);font-size:13px;margin-left:6px}
|
|
65
|
-
.tally .p{color:var(--
|
|
69
|
+
.tally .p{color:var(--verified)} .tally .f{color:var(--caught)} .tally .b{color:var(--fg-40)} .tally .t{color:var(--fg-50)}
|
|
66
70
|
.verdict-line{font-size:12px;margin:4px 0 0}
|
|
67
|
-
.verdict-line.green{color:var(--
|
|
68
|
-
.legend{display:flex;gap:18px;margin:8px 0 0;font-size:
|
|
71
|
+
.verdict-line.green[data-evidence='measured']{color:var(--verified)} .verdict-line.red{color:var(--caught)}
|
|
72
|
+
.legend{display:flex;gap:18px;margin:8px 0 0;font-size:12px;color:var(--fg-50);flex-wrap:wrap}
|
|
69
73
|
.legend b{font-family:var(--mono)}
|
|
70
|
-
h2.section{font-size:
|
|
74
|
+
h2.section{font-size:12px;text-transform:uppercase;letter-spacing:.12em;color:var(--fg-40);margin:26px 0 10px;font-weight:600}
|
|
71
75
|
.verifiers{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px}
|
|
72
|
-
.vchip{border:1px solid var(--border);border-radius:999px;padding:4px 11px;font-size:
|
|
76
|
+
.vchip{border:1px solid var(--border);border-radius:999px;padding:4px 11px;font-size:12px;color:var(--fg-70)}
|
|
73
77
|
.vchip code{font-family:var(--mono);color:var(--accent)}
|
|
74
|
-
table.matrix{width:100%;border-collapse:collapse;font-size:
|
|
78
|
+
table.matrix{width:100%;border-collapse:collapse;font-size:12px;border:1px solid var(--border-soft);border-radius:var(--radius);overflow:hidden}
|
|
75
79
|
table.matrix th,table.matrix td{padding:9px 11px;text-align:left;border-bottom:1px solid var(--border-soft)}
|
|
76
80
|
table.matrix thead th{font-size:10.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--fg-40);background:var(--bg-1);font-weight:600;vertical-align:bottom}
|
|
77
81
|
table.matrix tbody tr:hover{background:rgba(255,255,255,.02)}
|
|
78
82
|
.unit-id{font-family:var(--mono);color:var(--fg)}
|
|
79
|
-
.unit-desc{color:var(--fg-50);font-size:
|
|
80
|
-
.needs{font-family:var(--mono);color:var(--fg-30);font-size:
|
|
83
|
+
.unit-desc{color:var(--fg-50);font-size:12px}
|
|
84
|
+
.needs{font-family:var(--mono);color:var(--fg-30);font-size:12px}
|
|
81
85
|
.cell{display:inline-flex;align-items:center;justify-content:center;width:26px;height:22px;border-radius:5px;font-size:12px;cursor:pointer;border:0;background:transparent;color:var(--fg-30);font-family:var(--mono)}
|
|
82
|
-
.cell.pass{background:rgba(
|
|
83
|
-
.cell.fail{background:rgba(
|
|
86
|
+
.cell.pass{background:rgba(127,195,154,.12);color:var(--verified)}
|
|
87
|
+
.cell.fail{background:rgba(226,130,127,.16);color:var(--caught);box-shadow:0 0 0 1px rgba(226,130,127,.5)}
|
|
84
88
|
.cell.fail.expected{animation:flag 1.6s infinite}
|
|
85
89
|
.cell.blocked{background:rgba(255,255,255,.05);color:var(--fg-40)}
|
|
86
|
-
@keyframes flag{0%,100%{box-shadow:0 0 0 1px rgba(
|
|
90
|
+
@keyframes flag{0%,100%{box-shadow:0 0 0 1px rgba(226,130,127,.5)}50%{box-shadow:0 0 0 1px rgba(226,130,127,1),0 0 12px rgba(226,130,127,.5)}}
|
|
87
91
|
.fixhdr{font-family:var(--mono);font-size:10.5px;color:var(--fg-50);writing-mode:vertical-rl;transform:rotate(180deg);white-space:nowrap;height:96px}
|
|
88
|
-
.fixhdr.probe{color:var(--
|
|
89
|
-
.probe-row td{background:rgba(
|
|
90
|
-
.probe-tag{font-size:
|
|
91
|
-
.note{color:var(--fg-40);font-size:
|
|
92
|
+
.fixhdr.probe{color:var(--caught)}
|
|
93
|
+
.probe-row td{background:rgba(226,130,127,.04)}
|
|
94
|
+
.probe-tag{font-size:10.5px;color:var(--caught);border:1px solid rgba(226,130,127,.4);border-radius:4px;padding:1px 5px;margin-left:8px;font-family:var(--mono)}
|
|
95
|
+
.note{color:var(--fg-40);font-size:12px;margin-top:8px}
|
|
92
96
|
.note code{font-family:var(--mono);color:var(--info)}
|
|
93
97
|
/* replay */
|
|
94
98
|
.grid2{display:grid;grid-template-columns:1.4fr 1fr;gap:18px;margin-top:6px}
|
|
95
99
|
.stage-wrap{border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--bg-1);position:relative}
|
|
96
|
-
.stage-wrap.v-pass{box-shadow:0 0 0 1px rgba(
|
|
97
|
-
.stage-wrap.v-fail{box-shadow:0 0 0 1px rgba(
|
|
100
|
+
.stage-wrap.v-pass{box-shadow:0 0 0 1px rgba(127,195,154,.4)}
|
|
101
|
+
.stage-wrap.v-fail{box-shadow:0 0 0 1px rgba(226,130,127,.55)}
|
|
98
102
|
.stage-wrap.v-blocked{box-shadow:0 0 0 1px rgba(255,255,255,.12)}
|
|
99
103
|
.titlebar{display:flex;align-items:center;gap:7px;padding:8px 12px;background:var(--bg-2);border-bottom:1px solid var(--border-soft)}
|
|
100
|
-
.tl-dot{width:11px;height:11px;border-radius:50%}.tl-dot.r{background
|
|
101
|
-
.tl-title{margin-left:8px;font-size:
|
|
104
|
+
.tl-dot{width:11px;height:11px;border-radius:50%}.tl-dot.r{background:var(--caught)}.tl-dot.y{background:var(--warn)}.tl-dot.g{background:var(--verified)}
|
|
105
|
+
.tl-title{margin-left:8px;font-size:12px;color:var(--fg-70)}.tl-title .dim{color:var(--fg-30);font-family:var(--mono)}
|
|
102
106
|
iframe#surface{width:100%;height:560px;border:0;display:block;background:var(--bg)}
|
|
103
|
-
.checklist{position:absolute;top:46px;right:14px;width:226px;background:rgba(0,0,0,.82);border:1px solid var(--border);border-radius:10px;padding:10px 12px;z-index:52;font-size:
|
|
107
|
+
.checklist{position:absolute;top:46px;right:14px;width:226px;background:rgba(0,0,0,.82);border:1px solid var(--border);border-radius:10px;padding:10px 12px;z-index:52;font-size:12px;backdrop-filter:blur(6px);opacity:0;transition:opacity .2s}
|
|
104
108
|
.checklist.on{opacity:1}
|
|
105
|
-
.checklist .cl-h{font-family:var(--mono);font-size:
|
|
109
|
+
.checklist .cl-h{font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--fg-40);margin-bottom:7px}
|
|
106
110
|
.checklist .cl-row{display:flex;align-items:flex-start;gap:7px;padding:3px 0;color:var(--fg-50);transition:color .2s}
|
|
107
111
|
.checklist .cl-row.run{color:var(--fg)}
|
|
108
112
|
.checklist .cl-row .cl-id{font-family:var(--mono);font-weight:700;width:22px}
|
|
109
113
|
.checklist .cl-row .cl-st{width:14px;text-align:center}
|
|
110
|
-
.checklist .cl-row.pass .cl-id{color:var(--
|
|
114
|
+
.checklist .cl-row.pass .cl-id{color:var(--verified)} .checklist .cl-row.fail .cl-id{color:var(--caught)} .checklist .cl-row.blocked .cl-id{color:var(--fg-40)}
|
|
111
115
|
.ring{position:fixed;border:2px solid var(--accent);border-radius:8px;box-shadow:0 0 0 4px rgba(255,154,118,.25),0 0 18px rgba(255,154,118,.6);pointer-events:none;z-index:50;animation:ringpulse 1s infinite;transition:all .25s ease}
|
|
112
116
|
@keyframes ringpulse{0%,100%{opacity:1}50%{opacity:.55}}
|
|
113
|
-
.caption{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.88);border:1px solid var(--border);border-radius:999px;padding:7px 16px;font-size:
|
|
117
|
+
.caption{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.88);border:1px solid var(--border);border-radius:999px;padding:7px 16px;font-size:12px;color:var(--fg);font-family:var(--mono);white-space:nowrap;z-index:51;opacity:0;transition:opacity .2s}
|
|
114
118
|
.caption.on{opacity:1}
|
|
115
119
|
.framestatus{padding:10px 14px;border-top:1px solid var(--border-soft);font-size:12px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
|
116
120
|
.framestatus .badge{font-family:var(--mono);font-weight:700;padding:2px 9px;border-radius:5px}
|
|
117
|
-
.badge.pass{color:var(--
|
|
118
|
-
.framestatus .issues{color:var(--
|
|
121
|
+
.badge.pass{color:var(--verified);background:rgba(127,195,154,.12)} .badge.fail{color:var(--caught);background:rgba(226,130,127,.14)} .badge.blocked{color:var(--fg-40);background:rgba(255,255,255,.05)}
|
|
122
|
+
.framestatus .issues{color:var(--caught);font-size:12px}
|
|
119
123
|
.framestatus .issues.blk{color:var(--fg-40)}
|
|
120
124
|
.replay-side h3{font-size:12px;color:var(--fg-70);margin:0 0 10px}
|
|
121
125
|
.ctrlrow{display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap}
|
|
122
126
|
.progress{height:5px;background:var(--bg-2);border-radius:3px;overflow:hidden;margin:8px 0 14px}
|
|
123
|
-
.progress>div{height:100%;width:0;background:
|
|
127
|
+
.progress>div{height:100%;width:0;background:var(--accent);transition:width var(--dur-max) var(--ease)}
|
|
124
128
|
.steplist{font-size:12px}
|
|
125
129
|
.steprow{display:flex;align-items:center;gap:8px;padding:5px 7px;border-radius:6px;cursor:pointer}
|
|
126
130
|
.steprow.active{background:var(--accent-wash)}
|
|
127
131
|
.steprow .st{width:16px;text-align:center}
|
|
128
|
-
.steprow .nm{font-family:var(--mono);font-size:
|
|
129
|
-
.steprow.fail .nm{color:var(--
|
|
130
|
-
.clipnote{margin-top:14px;font-size:
|
|
132
|
+
.steprow .nm{font-family:var(--mono);font-size:12px;color:var(--fg-70)}
|
|
133
|
+
.steprow.fail .nm{color:var(--caught)}
|
|
134
|
+
.clipnote{margin-top:14px;font-size:12px;color:var(--fg-40)}
|
|
131
135
|
.clipnote code{font-family:var(--mono);color:var(--info)}
|
|
132
136
|
details.manifest{margin-top:26px;border:1px solid var(--border-soft);border-radius:var(--radius);padding:10px 14px}
|
|
133
137
|
details.manifest summary{cursor:pointer;color:var(--fg-50);font-size:12px}
|
|
134
|
-
details.manifest pre{font-family:var(--mono);font-size:
|
|
138
|
+
details.manifest pre{font-family:var(--mono);font-size:12px;color:var(--fg-50);overflow:auto;margin:10px 0 0;max-height:340px}
|
|
135
139
|
.hidden{display:none !important}
|
|
140
|
+
/* S17 · prefers-reduced-motion: ALL motion collapses, information stays
|
|
141
|
+
(same rule the product page ships). */
|
|
142
|
+
@media (prefers-reduced-motion: reduce){
|
|
143
|
+
*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}
|
|
144
|
+
}
|
|
136
145
|
</style>
|
|
137
146
|
</head>
|
|
138
147
|
<body data-verify-page="dashboard">
|
|
139
148
|
<header class="top">
|
|
140
|
-
<span class="logo"><span class="
|
|
149
|
+
<span class="logo"><span class="name">codehere</span></span>
|
|
141
150
|
<span class="route">/verify</span>
|
|
142
151
|
<span class="pill local">● local · zero telemetry</span>
|
|
143
152
|
<span class="spacer"></span>
|
|
@@ -152,10 +161,10 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
152
161
|
</div>
|
|
153
162
|
<div class="verdict-line" id="verdict-line"></div>
|
|
154
163
|
<div class="legend">
|
|
155
|
-
<span><b style="color:var(--
|
|
156
|
-
<span><b style="color:var(--
|
|
164
|
+
<span><b style="color:var(--verified)">PASS</b> observed & honest</span>
|
|
165
|
+
<span><b style="color:var(--caught)">FAIL</b> observed & lying</span>
|
|
157
166
|
<span><b style="color:var(--fg-40)">BLOCKED</b> 0 units to observe — never a silent green</span>
|
|
158
|
-
<span><b
|
|
167
|
+
<span><b>probe</b> designed-to-fail self-test</span>
|
|
159
168
|
</div>
|
|
160
169
|
|
|
161
170
|
<h2 class="section">Pluggable verifiers</h2>
|
|
@@ -169,16 +178,16 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
169
178
|
<table class="matrix" id="matrix"><thead></thead><tbody></tbody></table>
|
|
170
179
|
<p class="note" id="probe-note"></p>
|
|
171
180
|
|
|
172
|
-
<h2 class="section">Feature coverage · v0
|
|
181
|
+
<h2 class="section">Feature coverage · v0 › v1 MVP — does each PRODUCT_STATE claim actually render?</h2>
|
|
173
182
|
<div class="actions">
|
|
174
183
|
<button class="run" id="run-cov">▶ Run coverage</button>
|
|
175
184
|
<span class="tally" id="cov-tally"><span class="t">not run yet</span></span>
|
|
176
185
|
</div>
|
|
177
186
|
<div class="verdict-line" id="cov-verdict"></div>
|
|
178
187
|
<div class="legend">
|
|
179
|
-
<span><b style="color:var(--
|
|
180
|
-
<span><b style="color:var(--
|
|
181
|
-
<span><b style="color:var(--warn)">PARTIAL-GAP</b>
|
|
188
|
+
<span><b style="color:var(--verified)">OBSERVED</b> claimed shipped & rendered</span>
|
|
189
|
+
<span><b style="color:var(--caught)">MISSING</b> claimed shipped, NOT rendered (doc lies)</span>
|
|
190
|
+
<span><b style="color:var(--warn)">PARTIAL-GAP</b> ◐ honest, not yet visible</span>
|
|
182
191
|
<span><b style="color:var(--fg-40)">SKIP</b> roadmap / future (correctly absent)</span>
|
|
183
192
|
</div>
|
|
184
193
|
<table class="matrix" id="cov-matrix"><thead><tr><th style="width:52%">Feature (PRODUCT_STATE id)</th><th class="needs" style="width:12%">tier</th><th class="needs" style="width:12%">surface</th><th>state</th></tr></thead><tbody></tbody></table>
|
|
@@ -200,13 +209,13 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
200
209
|
<h3>Replay controls</h3>
|
|
201
210
|
<div class="ctrlrow">
|
|
202
211
|
<button class="linkbtn" id="play">▶ Play</button>
|
|
203
|
-
<button class="linkbtn" id="skip"
|
|
204
|
-
<button class="linkbtn" id="record">&#
|
|
212
|
+
<button class="linkbtn" id="skip">Skip ›</button>
|
|
213
|
+
<button class="linkbtn" id="record">● Record clips</button>
|
|
205
214
|
<span class="hint" id="dwell-label">dwell 1500ms</span>
|
|
206
215
|
</div>
|
|
207
216
|
<div class="progress"><div id="prog"></div></div>
|
|
208
217
|
<div class="steplist" id="steplist"></div>
|
|
209
|
-
<div class="clipnote">Each fixture
|
|
218
|
+
<div class="clipnote">Each fixture gets its own <code>.webm</code> (in-browser, pick this tab). Headless: <code>node scripts/record-replay.mjs</code> hands you one webm, no setup.</div>
|
|
210
219
|
</div>
|
|
211
220
|
</div>
|
|
212
221
|
</div>
|
|
@@ -221,7 +230,11 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
221
230
|
(function(){
|
|
222
231
|
"use strict";
|
|
223
232
|
|
|
224
|
-
/* -------- fixtures: the seeded sessions (the product's real surfaces) --------
|
|
233
|
+
/* -------- fixtures: the seeded sessions (the product's real surfaces) --------
|
|
234
|
+
S20 · the two lens columns re-observe the FAILED fixture with the Review /
|
|
235
|
+
Status lens active (sid = the session, lens = applied via the page's own
|
|
236
|
+
applyLens before reading): every invariant — H9/H13/H14/H15 included —
|
|
237
|
+
must hold under each lens. Honesty must survive the recomposition. */
|
|
225
238
|
var FIXTURES = [
|
|
226
239
|
{ id:"demo-rate-limit-shipped-v0.9", label:"rate-limit-shipped" },
|
|
227
240
|
{ id:"demo-recovery-test-failed-v0.9", label:"recovery-failed" },
|
|
@@ -230,17 +243,25 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
230
243
|
{ id:"demo-stalled-opencode-hung-v1", label:"stalled-hung" },
|
|
231
244
|
{ id:"demo-non-git-honest-v1", label:"non-git-honest" },
|
|
232
245
|
{ id:"demo-running-streaming-v0.9", label:"running-stream" },
|
|
246
|
+
{ id:"lens-review-failed", sid:"demo-recovery-test-failed-v0.9", lens:"review", label:"review lens \\u00b7 failed" },
|
|
247
|
+
{ id:"lens-status-failed", sid:"demo-recovery-test-failed-v0.9", lens:"status", label:"status lens \\u00b7 failed" },
|
|
233
248
|
{ id:"demo-probe-lie-v1", label:"probe-lie", probe:true, expectFail:"H5" }
|
|
234
249
|
];
|
|
235
250
|
|
|
236
251
|
/* -------- invariants: the honesty contracts (the "units") --------
|
|
237
252
|
needs(s) -> can this invariant observe anything here? (else BLOCKED)
|
|
238
253
|
check(s) -> [] when honest, or violation strings when lying (FAIL) */
|
|
254
|
+
/* LOW #23 (atomic audit) · there is no H4 — it was never built; numbering
|
|
255
|
+
jumps H3->H5 and is kept STABLE because H-ids are referenced by docs,
|
|
256
|
+
fixtures (expectFail) and evidence bundles. Do not renumber. */
|
|
239
257
|
var INVARIANTS = [
|
|
240
258
|
{ id:"H1", title:"no-verified-without-git", needs:"verifier",
|
|
241
259
|
desc:"verdict is not 'verified' when git is untracked",
|
|
242
260
|
can:function(s){ return s.verifiers.length>0; },
|
|
243
|
-
|
|
261
|
+
/* R11/M2 · the rule itself is the VerdictService's, interpolated at
|
|
262
|
+
build time — this harness EXECUTES verify-claim's own predicate
|
|
263
|
+
instead of re-encoding it, so engine and harness can't drift. */
|
|
264
|
+
check:function(s){ var violates=(${violatesGitInvariant.toString()}); return s.verifiers.filter(function(v){ return violates(v.verdict, v.gittracked!=="false"); }).map(function(){ return "verifier verdict=verified while git untracked (overclaim)"; }); } },
|
|
244
265
|
{ id:"H2", title:"say-never-bare", needs:"say",
|
|
245
266
|
desc:"every reasoning bubble has real words, not the bare label",
|
|
246
267
|
can:function(s){ return s.says.length>0; },
|
|
@@ -256,7 +277,97 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
256
277
|
{ id:"H6", title:"fail-has-honest-reason", needs:"terminal-fail",
|
|
257
278
|
desc:"a failed / killed / crashed run shows a plain reason, never a bare dot",
|
|
258
279
|
can:function(s){ return s.terminalFail; },
|
|
259
|
-
check:function(s){ return s.hasExplanation ? [] : ["failed session shows no recovery / crash / stall explanation"]; } }
|
|
280
|
+
check:function(s){ return s.hasExplanation ? [] : ["failed session shows no recovery / crash / stall explanation"]; } },
|
|
281
|
+
{ id:"H8", title:"observer-honest", needs:"observer rail",
|
|
282
|
+
desc:"the observer never shows a stale session as live, never renders a blank box, never seeds fake rows (Thariq loop-2 design B)",
|
|
283
|
+
can:function(s){ return !!s.observer; },
|
|
284
|
+
check:function(s){
|
|
285
|
+
var o=s.observer, out=[];
|
|
286
|
+
if(o.mode==='empty'){
|
|
287
|
+
if(o.rowStates.length>0) out.push("empty observer rendered "+o.rowStates.length+" row(s) · should seed NO fake rows");
|
|
288
|
+
if(o.teachLen<20) out.push("empty observer is a blank box · it must teach the 60-second path");
|
|
289
|
+
} else if(o.mode==='active'){
|
|
290
|
+
var liveRows=o.rowStates.filter(function(st){ return st==='live'; }).length;
|
|
291
|
+
if(o.liveClaimed!==liveRows) out.push("header claims "+o.liveClaimed+" live but "+liveRows+" row(s) are marked live (stale-as-live)");
|
|
292
|
+
if(o.textLen<10) out.push("active observer rendered with no content (empty container)");
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
} },
|
|
296
|
+
{ id:"H12", title:"follow-up-never-lies", needs:"detail composer",
|
|
297
|
+
desc:"the follow-up box is enabled only when the server can deliver the message; a disabled box carries a plain reason \u00b7 never an invitation to send into a void (S1)",
|
|
298
|
+
can:function(s){ return !!s.followUp && !!s.followUp.state; },
|
|
299
|
+
check:function(s){
|
|
300
|
+
var f=s.followUp, out=[];
|
|
301
|
+
if(f.state==='disabled-honest' && f.placeholderLen<20) out.push("follow-up disabled but the reason placeholder is missing/too thin");
|
|
302
|
+
if(f.state==='disabled-honest' && !f.disabled) out.push("composer claims disabled-honest but the box is still enabled (the invitation lie)");
|
|
303
|
+
if(f.state==='enabled' && f.disabled) out.push("composer stamped enabled but the box is disabled (silent dead composer)");
|
|
304
|
+
return out;
|
|
305
|
+
} },
|
|
306
|
+
{ id:"H11", title:"running-task-stoppable", needs:"a running session",
|
|
307
|
+
desc:"a running task shows a Stop control on its detail · you can stop an agent the moment you stop trusting it (S2)",
|
|
308
|
+
can:function(s){ return s.status==='running'; },
|
|
309
|
+
check:function(s){ return s.hasStopControl ? [] : ["running task renders no Stop control on the detail (unstoppable runaway)"]; } },
|
|
310
|
+
{ id:"H10", title:"cost-estimate-honest", needs:"composer cost chip",
|
|
311
|
+
desc:"the cost chip is computed (free / recent-real / typical), labeled as what it is, and its tooltip never claims a computation that didn't happen (S7)",
|
|
312
|
+
can:function(s){ return s.costEst && s.costEst.length>0; },
|
|
313
|
+
check:function(s){
|
|
314
|
+
var out=[];
|
|
315
|
+
s.costEst.forEach(function(c){
|
|
316
|
+
if(!c.mode) out.push("cost chip rendered with no computed mode (estimator never ran) \\u00b7 text: '"+c.text+"'");
|
|
317
|
+
else if(c.mode==='free' && c.text.indexOf('$0')===-1) out.push("free-mode chip does not say $0");
|
|
318
|
+
else if(c.mode==='typical' && c.text.indexOf('typical')===-1) out.push("flat-fallback chip not labeled 'typical' \\u00b7 a flat figure dressed as a computation");
|
|
319
|
+
else if(c.mode==='typical' && /prompt length/i.test(c.title)) out.push("typical-mode tooltip claims prompt-length computation that did not happen");
|
|
320
|
+
else if(c.mode==='recent' && c.text.indexOf('~$')===-1) out.push("recent-mode chip missing the computed ~$ figure");
|
|
321
|
+
});
|
|
322
|
+
return out;
|
|
323
|
+
} },
|
|
324
|
+
{ id:"H9", title:"no-green-on-failed", needs:"verifier on a failed run",
|
|
325
|
+
desc:"a failed / killed / crashed task never wears the green 'verified' block · an honest report about a failure is not a success (S5)",
|
|
326
|
+
can:function(s){ return s.terminalFail && s.verifiers.length>0; },
|
|
327
|
+
check:function(s){ return s.verifiers.filter(function(v){ return v.verdict==="verified"; }).map(function(){ return "status=failed task renders a green verdict=verified block (fake-green on the proof surface)"; }); } },
|
|
328
|
+
{ id:"H13", title:"hero-numbers-honest", needs:"home hero",
|
|
329
|
+
desc:"the home hero's claimed counts (agents/verified/caught/running) equal the counts derivable from the task list the page itself holds; the first-run hero never hides real work (S12)",
|
|
330
|
+
can:function(s){ return !!(s.hero && s.hero.expected); },
|
|
331
|
+
check:function(s){
|
|
332
|
+
var h=s.hero, e=h.expected, out=[];
|
|
333
|
+
if(h.mode==="first-run"){
|
|
334
|
+
if(e.today>0) out.push("hero shows the first-run story while "+e.today+" task(s) started today (real work hidden)");
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
["agents","verified","caught","stopped","running"].forEach(function(k){
|
|
338
|
+
if(h[k]!==e[k]) out.push("hero claims "+k+"="+h[k]+" but the task list derives "+e[k]);
|
|
339
|
+
});
|
|
340
|
+
return out;
|
|
341
|
+
} },
|
|
342
|
+
{ id:"H14", title:"decisions-honest", needs:"decision-queue state",
|
|
343
|
+
desc:"the NEEDS-YOUR-DECISION queue's claimed N equals the count derivable from the page's own state (pending write-passes + verifier-broken verdicts today + failed runs with retry available today); zero derivable decisions means the section is absent entirely (S13)",
|
|
344
|
+
can:function(s){ return !!(s.decisions && s.decisions.expected!==null); },
|
|
345
|
+
check:function(s){
|
|
346
|
+
var d=s.decisions, out=[];
|
|
347
|
+
if(d.expected===0){
|
|
348
|
+
if(d.present) out.push("decision queue renders ("+d.rows+" row(s), claims N="+d.n+") while 0 decisions are derivable \\u00b7 the section must be absent when nothing needs you");
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
if(!d.present){ out.push(d.expected+" decision(s) derivable from page state but the queue is absent (real decisions hidden)"); return out; }
|
|
352
|
+
if(d.n!==d.expected) out.push("queue claims N="+d.n+" but the page state derives "+d.expected);
|
|
353
|
+
if(d.rows!==d.n) out.push("queue claims N="+d.n+" but renders "+d.rows+" row(s)");
|
|
354
|
+
return out;
|
|
355
|
+
} },
|
|
356
|
+
{ id:"H15", title:"glyph-discipline", needs:"rendered surface",
|
|
357
|
+
desc:"no emoji/pictograph codepoint anywhere in the rendered CHROME, observed in the default AND expanded history/practice states \\u00b7 status and meaning are carried by the sanctioned geometric glyph set (\\u2713 \\u2715 \\u25CF \\u25CB \\u2022 \\u2298 \\u203A), words and color, never emoji-as-icon; user/run content ([data-user-content]) is the user's own text, not chrome, and is excluded (S19)",
|
|
358
|
+
can:function(s){ return !!(s.glyphScan && s.glyphScan.scanned); },
|
|
359
|
+
check:function(s){ return s.glyphScan.hits; } },
|
|
360
|
+
{ id:"H7", title:"recovery-buttons-must-not-lie", needs:"recovery card",
|
|
361
|
+
desc:"every ENABLED recovery action does a real thing; a disabled one carries its reason · no button narrates an action it doesn't perform (Thariq loop-1)",
|
|
362
|
+
can:function(s){ return s.recoveryButtons && s.recoveryButtons.length>0; },
|
|
363
|
+
check:function(s){
|
|
364
|
+
var out = [];
|
|
365
|
+
s.recoveryButtons.forEach(function(b){
|
|
366
|
+
if(b.bareToast) out.push("recovery '"+b.action+"' is enabled but only fires a toast · it lies about acting");
|
|
367
|
+
if(b.disabled && !b.hasReason) out.push("recovery '"+b.action+"' is disabled with no reason (silent dead button)");
|
|
368
|
+
});
|
|
369
|
+
return out;
|
|
370
|
+
} }
|
|
260
371
|
];
|
|
261
372
|
|
|
262
373
|
var PROTO_VERSION = "codehere-verify/1";
|
|
@@ -264,92 +375,344 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
264
375
|
/* -------- feature coverage: every granular PRODUCT_STATE claim, v0->v1 --------
|
|
265
376
|
Turns the ~150-row inventory's claims into watchable checks against the REAL
|
|
266
377
|
surface. status drives the verdict:
|
|
267
|
-
shipped (
|
|
268
|
-
partial (
|
|
269
|
-
roadmap (
|
|
270
|
-
future (
|
|
378
|
+
shipped ([shipped]/[green]) -> element MUST render -> OBSERVED, else MISSING (the claim is a lie)
|
|
379
|
+
partial ([partial]) -> render if present -> OBSERVED, else PARTIAL-GAP (honest, not a lie)
|
|
380
|
+
roadmap ([roadmap]) -> SKIP-ROADMAP (planned, not built — correctly absent)
|
|
381
|
+
future ([future]/cut) -> SKIP-FUTURE (deferred by design / cut)
|
|
271
382
|
surface: 'list' = dashboard · 'detail' = opens demo-rate-limit-shipped-v0.9.
|
|
272
383
|
sel = real CSS selector confirmed against the live DOM (not invented). */
|
|
273
384
|
var COV_FIXTURE = "demo-rate-limit-shipped-v0.9";
|
|
385
|
+
/* Inject a synthetic 3-task batch into the page's task STATE (the banner-
|
|
386
|
+
drive honesty bar: state occurs -> the real render path must produce the
|
|
387
|
+
surface; never DOM injection). Restored by __unseedCovBatch. Without this,
|
|
388
|
+
a home that never ran the batch seed reported the two v1.0 rows MISSING
|
|
389
|
+
even though the render path is real (found in the It-4 joint ceremony). */
|
|
390
|
+
function __seedCovBatch(w){
|
|
391
|
+
try{
|
|
392
|
+
var tk = w._cached && w._cached.tasks; if(!tk) return false;
|
|
393
|
+
if (w.__covSavedPersisted !== undefined) return true; // already seeded this run
|
|
394
|
+
w.__covSavedPersisted = tk.persisted;
|
|
395
|
+
var now = Date.now()-300000;
|
|
396
|
+
var mk = function(id, ag, st){ return { sessionId:id, agent:ag, prompt:"coverage probe "+id, cwd:"/cov-batch-probe", startedAt:now, status:st, batchId:"cov-batch-1", steps:[], artifacts:{}, provenance:"persisted", durationMs:1000, completedAt: st==="running"?0:now+1000 }; };
|
|
397
|
+
tk.persisted = (tk.persisted||[]).concat([ mk("cov-b1","claude-code","completed"), mk("cov-b2","codex","running"), mk("cov-b3","gemini","failed") ]);
|
|
398
|
+
return true;
|
|
399
|
+
}catch(e){ return false; }
|
|
400
|
+
}
|
|
401
|
+
function __unseedCovBatch(w){
|
|
402
|
+
try{
|
|
403
|
+
if (w.__covSavedPersisted === undefined) return;
|
|
404
|
+
if (w._cached && w._cached.tasks) w._cached.tasks.persisted = w.__covSavedPersisted;
|
|
405
|
+
delete w.__covSavedPersisted;
|
|
406
|
+
}catch(e){}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* It-4 V1-MULTITASK/V1-FANOUT-ALL drive helper. Finds a cwd in the
|
|
410
|
+
/api/tasks data (real or coverage-injected) whose turns include a batchId
|
|
411
|
+
shared by >=2 tasks, then drives the real openRoom() so renderRoom()
|
|
412
|
+
produces the live .project-rollup + .fanout-all surface. Returns the cwd
|
|
413
|
+
it opened, or null if no batch is present. */
|
|
414
|
+
function __openBatchRoom(w){
|
|
415
|
+
try{
|
|
416
|
+
var all = (w.allS ? w.allS(w._cached && w._cached.tasks || {}) : []);
|
|
417
|
+
var byBatch = {};
|
|
418
|
+
for(var i=0;i<all.length;i++){
|
|
419
|
+
/* S18 · practice (mockRun) batches collapse behind the practice row in
|
|
420
|
+
rooms — they no longer render a rollup, so the drive must open a
|
|
421
|
+
REAL batch room (the seeded cov-batch qualifies). Same honest rule
|
|
422
|
+
as renderRoom, applied to both sides. */
|
|
423
|
+
var t=all[i]; if(!t.batchId || t.mockRun) continue;
|
|
424
|
+
(byBatch[t.batchId]=byBatch[t.batchId]||[]).push(t);
|
|
425
|
+
}
|
|
426
|
+
for(var bid in byBatch){
|
|
427
|
+
if(byBatch[bid].length>=2){
|
|
428
|
+
var cwd=byBatch[bid][0].cwd; if(!cwd) continue;
|
|
429
|
+
if(w.openRoom){ w.openRoom(encodeURIComponent(cwd)); return cwd; }
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}catch(e){}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
274
435
|
var FEATURES = [
|
|
275
436
|
{ id:"F-AUDIT-FOOTER", name:"Audit chain footer", surface:"list", sel:".audit-chain-panel", status:"shipped", tier:"v0.7" },
|
|
276
|
-
{ id:"F-BUDGET-CHIP", name:"Budget pill", surface:"list", sel:".budget", status:"shipped", tier:"v0.7"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
437
|
+
{ id:"F-BUDGET-CHIP", name:"Budget pill", surface:"list", sel:".budget", status:"shipped", tier:"v0.7",
|
|
438
|
+
/* .budget fills its text only when a daily budget is set (upBud). Real
|
|
439
|
+
~/.codehere often has no cap set, so it flaps empty. Drive the REAL
|
|
440
|
+
upBud() over a set-budget pricing state (60% spent · safe), restore. */
|
|
441
|
+
drive:function(w,d){ try{ w.__covBud=w._cached.pricing; w._cached.pricing={dailyBudgetCents:500,spentTodayCents:300}; w.upBud(); }catch(e){} },
|
|
442
|
+
undrive:function(w,d){ try{ w._cached.pricing=w.__covBud; w.upBud(); }catch(e){} } },
|
|
443
|
+
{ id:"F-BUDGET-WARN-BANNER", name:"Budget warning banner", surface:"list", sel:".budget-banner", status:"partial", tier:"v0.6",
|
|
444
|
+
/* drive the REAL upBud() over an over-budget pricing state, observe the
|
|
445
|
+
banner, restore. Same legitimacy as F-CUSTOM-VIEW-BANNER's class drive. */
|
|
446
|
+
drive:function(w,d){ try{ w.__covPricing=w._cached.pricing; w._cached.pricing={dailyBudgetCents:500,spentTodayCents:500}; w.upBud(); }catch(e){} },
|
|
447
|
+
undrive:function(w,d){ try{ w._cached.pricing=w.__covPricing; w.upBud(); }catch(e){} } },
|
|
448
|
+
/* S13 · at rest the home shows TODAY one-line rows only; demos + earlier
|
|
449
|
+
work collapse behind the history row. Card rows are observed by
|
|
450
|
+
expanding history through the REAL toggle (window.__histExpand), the
|
|
451
|
+
same path the user's click takes — never DOM injection. */
|
|
452
|
+
{ id:"F-CARD-AGENT-BORDER", name:"Per-vendor class on task row (S12: identity is the monochrome name, no hue)", surface:"list", sel:".task-card.agent-claude-code", status:"shipped", tier:"v0.9",
|
|
453
|
+
drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
|
|
454
|
+
undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
|
|
455
|
+
{ id:"F-CARD-CLICK-DETAIL", name:"Click card/row -> detail view", surface:"list", sel:".task-card", status:"shipped", tier:"v0.6",
|
|
456
|
+
drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
|
|
457
|
+
undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
|
|
458
|
+
{ id:"F-CARD-OUTCOME-CHIP", name:"Outcome on the card (the .tc-stat pill: verdict word + checks, right-aligned with cost)", surface:"list", sel:".tcard .tc-stat", status:"shipped", tier:"v0.7",
|
|
459
|
+
drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
|
|
460
|
+
undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
|
|
461
|
+
{ id:"F-CUSTOM-VIEW-BANNER", name:"Custom view banner", surface:"list", sel:".custom-view-banner", status:"shipped", tier:"v0.8",
|
|
462
|
+
drive:function(w,d){ d.body.classList.add("custom-view"); },
|
|
463
|
+
undrive:function(w,d){ d.body.classList.remove("custom-view"); } },
|
|
282
464
|
{ id:"F-DAEMON-HEARTBEAT", name:"Local pill auto-update", surface:"list", sel:".local-pill", status:"shipped", tier:"v0.6" },
|
|
283
|
-
{ id:"F-FAILOVER-BANNER", name:"Failover banner", surface:"list", sel:".failover-banner", status:"partial", tier:"v0.7"
|
|
284
|
-
|
|
285
|
-
|
|
465
|
+
{ id:"F-FAILOVER-BANNER", name:"Failover banner", surface:"list", sel:".failover-banner.show", status:"partial", tier:"v0.7",
|
|
466
|
+
/* drive the banner's OWN render (window.__fbRender) with a real offer
|
|
467
|
+
shape — same object /api/failover-offers returns — so we observe the
|
|
468
|
+
live render path, not a fake DOM injection. */
|
|
469
|
+
drive:function(w,d){ try{ w.__fbRender([{ sessionId:"cov-failover", target:"codex", signal:{ fromAgent:"claude-code", errorCode:"rate_limited" } }]); }catch(e){} },
|
|
470
|
+
undrive:function(w,d){ try{ w.__fbRender([]); }catch(e){} } },
|
|
471
|
+
/* S13 · filters moved into the ⌘K palette (rule 9). Observed by opening
|
|
472
|
+
the REAL palette (window.openPalette — the same function ⌘K runs) and
|
|
473
|
+
asserting the filter commands render; the command's Enter runs the
|
|
474
|
+
same setFilter() the tabs ran. */
|
|
475
|
+
{ id:"F-FILTER-PILLS", name:"All/Running/Done/Failed filters (S13: \u2318K command EXECUTED \u203A the removable token is the observed effect)", surface:"list", sel:".ftoken", status:"shipped", tier:"v0.7",
|
|
476
|
+
/* Refuter (S13) · presence-only re-pointing was vacuous: a lobotomized
|
|
477
|
+
command element would still pass. This drive runs the REAL command
|
|
478
|
+
(palette open \u2192 click filter-failed) and the row observes its
|
|
479
|
+
CONSEQUENCE (.ftoken) \u2014 a broken run closure now reads MISSING. */
|
|
480
|
+
drive:function(w,d){ try{ w.openPalette(); var c=d.querySelector("[data-verify-cmd='filter-failed']"); if(c) c.click(); }catch(e){} },
|
|
481
|
+
undrive:function(w,d){ try{ var x=d.querySelector(".ftoken .x"); if(x) x.click(); w.closePalette(); }catch(e){} } },
|
|
482
|
+
{ id:"F-INDEXING-BANNER", name:"First-run indexing banner", surface:"list", sel:".indexing-banner", status:"partial", tier:"v0.7",
|
|
483
|
+
/* partial by nature: this banner only shows on a genuinely fresh home
|
|
484
|
+
(<3 sessions). The drive reproduces that fresh-home state — empties
|
|
485
|
+
_cached.tasks + clears the sessionStorage dismissal — runs the REAL
|
|
486
|
+
upIdx(), observes, then restores. */
|
|
487
|
+
drive:function(w,d){ try{ w.__covTasks=w._cached.tasks; w._cached.tasks={spawned:[],persisted:[],sessions:[]}; try{w.sessionStorage.removeItem("codehere_indexing_dismissed");}catch(e){} w.upIdx(); }catch(e){} },
|
|
488
|
+
undrive:function(w,d){ try{ w._cached.tasks=w.__covTasks; w.upIdx(); }catch(e){} } },
|
|
286
489
|
{ id:"F-LOCAL-PILL", name:"Local-first / zero-telemetry pill", surface:"list", sel:".local-pill", status:"shipped", tier:"v0.7" },
|
|
287
490
|
{ id:"F-LOGO", name:"Brand wordmark", surface:"list", sel:".logo .name", status:"shipped", tier:"v0.7" },
|
|
288
|
-
{ id:"F-NEW-TASK-BTN", name:"
|
|
289
|
-
|
|
491
|
+
{ id:"F-NEW-TASK-BTN", name:"New task (S13: first ⌘K command, focuses the composer)", surface:"list", sel:"[data-verify-cmd='new-task']", status:"shipped", tier:"v0.7",
|
|
492
|
+
drive:function(w,d){ try{ w.openPalette(); }catch(e){} },
|
|
493
|
+
undrive:function(w,d){ try{ w.closePalette(); }catch(e){} } },
|
|
494
|
+
{ id:"F-OBSERVED-THREAD", name:"Observed session as thread", surface:"list", sel:".obs-row", status:"shipped", tier:"v0.7",
|
|
495
|
+
/* S9 made practice mode honest: the mock chain starts EMPTY, so the
|
|
496
|
+
observer correctly renders its teach-state and this row has no .obs-row
|
|
497
|
+
to observe — it used to pass only because past mock runs had POLLUTED
|
|
498
|
+
the real chain with host sessions. Banner-drive honesty bar (same as
|
|
499
|
+
__seedCovBatch): seed the STATE through the REAL render path (the
|
|
500
|
+
exact function /api/host-sessions feeds), clearly labeled as a
|
|
501
|
+
coverage probe, restored from the real API after. Never DOM. */
|
|
502
|
+
/* S13 · the observer rests as one calm line; .obs-row renders in the
|
|
503
|
+
EXPANDED rail — drive the real expand flag with the probe session. */
|
|
504
|
+
drive:function(w,d){ w._obsOpen=true; w.renderHostSessions({ sessions:[{ sessionId:"coverage-probe-host-session", shortId:"cov-probe", lastSeen:new Date().toISOString(), toolMix:{ Bash:3, Read:2 }, blockedCalls:0, criticalCalls:0, totalCalls:5, durationMs:60000, risk:"low" }] }); },
|
|
505
|
+
undrive:function(w,d){ try{ w._obsOpen=false; fetch("/api/host-sessions").then(function(r){ return r.json(); }).then(function(h){ w.renderHostSessions(h); }); }catch(e){} } },
|
|
290
506
|
{ id:"F-OSS-FOOTER", name:"OSS footer", surface:"list", sel:".oss-footer", status:"shipped", tier:"v0.7" },
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
507
|
+
/* S20 · the five personas died; three lenses replace them. Each row is
|
|
508
|
+
EFFECT-observing (the F-FILTER-PILLS bar): the drive EXECUTES the real
|
|
509
|
+
⌘K lens command (palette open -> click -> applyLens) and the sel pins
|
|
510
|
+
the RECOMPOSITION that command must produce — a lobotomized command, a
|
|
511
|
+
missing body[data-lens], or a lens that failed to recompose all read
|
|
512
|
+
MISSING. State captured + restored in undrive. */
|
|
513
|
+
// F-LENS-DEFAULT/REVIEW/STATUS removed 2026-06-20 — the lens mode-switch was
|
|
514
|
+
// cut for ONE consistent conversation-first view (no more UI reshaping
|
|
515
|
+
// between visits). The detail's honest verdict + evidence disclosure carry
|
|
516
|
+
// what Review/Status used to surface.
|
|
517
|
+
{ id:"F-PIN-DEMO", name:"Demo-shape sessions pinned to FEATURED (S13: inside expanded history)", surface:"list", sel:".task-card.tc-pinned", status:"shipped", tier:"v0.9",
|
|
518
|
+
drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
|
|
519
|
+
undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
|
|
520
|
+
{ id:"F-PORT-FAILOVER-NOTICE", name:"Port-collision UI surface (banner render path shared with failover; the PORT-collision trigger itself is not simulated)", surface:"list", sel:".failover-banner.show", status:"partial", tier:"v0.7", // shares the failover-banner surface; driven via window.__fbRender (see F-FAILOVER-BANNER). Honest = partial: the port-collision PATH itself isn't simulated, only the shared banner render.
|
|
521
|
+
drive:function(w,d){ try{ w.__fbRender([{ sessionId:"cov-port", target:"codex", signal:{ fromAgent:"claude-code", errorCode:"port_in_use" } }]); }catch(e){} },
|
|
522
|
+
undrive:function(w,d){ try{ w.__fbRender([]); }catch(e){} } },
|
|
523
|
+
// F-RESHAPE-BTN removed 2026-06-20 — Reshape mode cut for one consistent view.
|
|
524
|
+
{ id:"F-SETTINGS-COG", name:"Settings drawer trigger (S13: ⌘K Settings command)", surface:"list", sel:"[data-verify-cmd='settings']", status:"shipped", tier:"v0.8",
|
|
525
|
+
drive:function(w,d){ try{ w.openPalette(); }catch(e){} },
|
|
526
|
+
undrive:function(w,d){ try{ w.closePalette(); }catch(e){} } },
|
|
527
|
+
{ id:"F-SHARE-BTN", name:"Share thread (S13: ⌘K Share command -> same signed-link modal)", surface:"list", sel:"[data-verify-cmd='share']", status:"shipped", tier:"v0.8",
|
|
528
|
+
drive:function(w,d){ try{ w.openPalette(); }catch(e){} },
|
|
529
|
+
undrive:function(w,d){ try{ w.closePalette(); }catch(e){} } },
|
|
530
|
+
{ id:"F-SQUAD-ROW", name:"Agent readiness (S13: strip died · ⌘K Agents command carries the real readiness)", surface:"list", sel:"[data-verify-cmd='agents']", status:"shipped", tier:"v0.7",
|
|
531
|
+
drive:function(w,d){ try{ w.openPalette(); }catch(e){} },
|
|
532
|
+
undrive:function(w,d){ try{ w.closePalette(); }catch(e){} } },
|
|
533
|
+
{ id:"F-TASK-CARD", name:"Task row (one-line, keeps .task-card contract)", surface:"list", sel:".task-card", status:"shipped", tier:"v0.7",
|
|
534
|
+
drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
|
|
535
|
+
undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
|
|
536
|
+
{ id:"F-TIME-GAP-PILL", name:"Brand pill + relative-time sub", surface:"list", sel:".time-gap", status:"shipped", tier:"v0.7",
|
|
537
|
+
/* Data-dependent (same hazard verify-page already calls out for goal groups):
|
|
538
|
+
.time-gap renders only BETWEEN tasks >2h apart (renderL). Real ~/.codehere
|
|
539
|
+
can legitimately have no such gap — a busy session clusters every task into
|
|
540
|
+
one <2h window (this exact flap, 2026-06-08) — so the live-DOM check flaps
|
|
541
|
+
MISSING on honest data. Observe it DETERMINISTICALLY instead, the same way
|
|
542
|
+
F-WORKTREE-ALERT does: inject two same-goal tasks 6h apart, render, observe,
|
|
543
|
+
restore. The render path is the REAL renderL — not a faked element. */
|
|
544
|
+
/* S12 · both fixtures sit 3 days back so they land in ONE chapter
|
|
545
|
+
(EARLIER) deterministically — the home now splits NOW/TODAY/EARLIER
|
|
546
|
+
and a now/now-6h pair could straddle midnight and split chapters. */
|
|
547
|
+
/* S13 · the gap rows live inside the EARLIER goal groups, which sit
|
|
548
|
+
behind the history row now — expand through the REAL toggle. */
|
|
549
|
+
/* S21 · same-title runs now collapse into ONE thread (groupByTitle), so a
|
|
550
|
+
time-gap between two runs of the SAME task no longer renders (they're one
|
|
551
|
+
thread) — correct. The .time-gap feature renders between DISTINCT tasks
|
|
552
|
+
>2h apart; the fixture uses two distinct-title tasks 6h apart to exercise
|
|
553
|
+
it (still same goal/cwd, so still one goal group). */
|
|
554
|
+
drive:function(w,d){ try{ w.__covTG=w._cached.tasks; var now=Date.now(); var mk=function(id,ts,p){return{sessionId:id,status:"completed",cwd:"/repo/cov-tg",startedAt:ts,prompt:p,agent:"claude-code",steps:[]};}; var base=now-3*24*3600*1000; w._cached.tasks={spawned:[],persisted:[mk("cov-tg-new",base,"coverage fixture new"),mk("cov-tg-old",base-6*3600*1000,"coverage fixture old")],sessions:[]}; w.__histExpand(); w.renderL(); }catch(e){} },
|
|
555
|
+
undrive:function(w,d){ try{ w._cached.tasks=w.__covTG; w.__histCollapse(); w.renderL(); }catch(e){} } },
|
|
556
|
+
{ id:"F-WORKTREE-ALERT", name:"Worktree alert", surface:"list", sel:".worktree-alert", status:"partial", tier:"v0.7",
|
|
557
|
+
/* upWk() fires when >=2 running sessions span different cwds and the odd
|
|
558
|
+
one has been running >5min. Drive that exact predicate over the REAL
|
|
559
|
+
upWk() with a synthetic 2-running-session state, observe, restore. */
|
|
560
|
+
drive:function(w,d){ try{ w.__covTasks=w._cached.tasks; var old=Date.now()-10*60*1000; w._cached.tasks={spawned:[],persisted:[{sessionId:"cov-wk-main",status:"running",cwd:"/repo/main",startedAt:old},{sessionId:"cov-wk-odd",status:"running",cwd:"/repo/feature-x",startedAt:old}],sessions:[]}; w.upWk(); }catch(e){} },
|
|
561
|
+
undrive:function(w,d){ try{ w._cached.tasks=w.__covTasks; w.upWk(); }catch(e){} } },
|
|
301
562
|
/* goal-first home + goal room (v1 spec · shared-room hand-off). Goal groups
|
|
302
563
|
render from REAL non-demo tasks (same data dependency as F-TIME-GAP-PILL);
|
|
303
564
|
room features are observed by driving the REAL openRoom() on the first
|
|
304
565
|
goal-head's data-goal-cwd, then closeRoom() — same pattern as showD(). */
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
566
|
+
/* S13 · goal groups own the EXPANDED history (today is flat one-line
|
|
567
|
+
rows); observed deterministically with the same injected-state drive
|
|
568
|
+
F-TIME-GAP-PILL uses — real renderL, real toggle, restored after. */
|
|
569
|
+
{ id:"F-GOAL-GROUP", name:"Goal grouping (S13: inside expanded history; heads still open rooms)", surface:"list", sel:".goal-group", status:"shipped", tier:"v1",
|
|
570
|
+
drive:function(w,d){ try{ w.__covGG=w._cached.tasks; var now=Date.now(); var mk=function(id,ts){return{sessionId:id,status:"completed",cwd:"/repo/cov-gg",startedAt:ts,prompt:"coverage fixture",agent:"claude-code",steps:[]};}; var base=now-3*24*3600*1000; w._cached.tasks={spawned:[],persisted:[mk("cov-gg-a",base),mk("cov-gg-b",base-3600*1000)],sessions:[]}; w.__histExpand(); w.renderL(); }catch(e){} },
|
|
571
|
+
undrive:function(w,d){ try{ w._cached.tasks=w.__covGG; w.__histCollapse(); w.renderL(); }catch(e){} } },
|
|
572
|
+
{ id:"F-GOAL-HEAD-OPEN", name:"Goal head opens its room (carries cwd)", surface:"list", sel:".goal-head[data-goal-cwd]", status:"shipped", tier:"v1",
|
|
573
|
+
drive:function(w,d){ try{ w.__covGH=w._cached.tasks; var now=Date.now(); var mk=function(id,ts){return{sessionId:id,status:"completed",cwd:"/repo/cov-gh",startedAt:ts,prompt:"coverage fixture",agent:"claude-code",steps:[]};}; var base=now-3*24*3600*1000; w._cached.tasks={spawned:[],persisted:[mk("cov-gh-a",base),mk("cov-gh-b",base-3600*1000)],sessions:[]}; w.__histExpand(); w.renderL(); }catch(e){} },
|
|
574
|
+
undrive:function(w,d){ try{ w._cached.tasks=w.__covGH; w.__histCollapse(); w.renderL(); }catch(e){} } },
|
|
575
|
+
{ id:"F-AGENT-PICK", name:"Composer agent picker (Auto + 5 agents)", surface:"list", sel:"button#agentpick-btn", status:"shipped", tier:"v1" },
|
|
576
|
+
/* S13 · the two new surfaces. The palette is driven through the REAL
|
|
577
|
+
opener (the same function ⌘K runs); the decision queue renders ONLY
|
|
578
|
+
when real decisions exist, so it's observed by injecting a failed-task
|
|
579
|
+
state through the real renderDecisions(), then restored. */
|
|
580
|
+
{ id:"F-CMDK-PALETTE", name:"⌘K command palette · grouped Act/View/Trust/Go, Enter runs the real function", surface:"list", sel:"[data-verify-unit='palette'] .prow[data-verify-cmd]", status:"shipped", tier:"v1",
|
|
581
|
+
drive:function(w,d){ try{ w.openPalette(); }catch(e){} },
|
|
582
|
+
undrive:function(w,d){ try{ w.closePalette(); }catch(e){} } },
|
|
583
|
+
{ id:"F-DECISION-QUEUE", name:"Needs-your-decision queue · absent when nothing needs you (H14 pins the count; this drive pins the PRESENT branch deterministically through the real renderDecisions)", surface:"list", sel:"#decision-queue .dq-row", status:"shipped", tier:"v1",
|
|
584
|
+
drive:function(w,d){ try{ w.__covDQ=w._cached.tasks; var now=Date.now()-600000; w._cached.tasks={spawned:[],persisted:[{sessionId:"cov-dq-fail",status:"failed",cwd:"/repo/cov-dq",startedAt:now,prompt:"coverage decision probe",agent:"claude-code",steps:[]}],sessions:[]}; w.renderDecisions(); }catch(e){} },
|
|
585
|
+
undrive:function(w,d){ try{ w._cached.tasks=w.__covDQ; w.renderDecisions(); }catch(e){} } },
|
|
586
|
+
/* S18 · the practice group's own regression net (the board must be able to
|
|
587
|
+
catch a regression in the exact story it ships). Two assertions in ONE
|
|
588
|
+
drive, both through the REAL render path:
|
|
589
|
+
(1) absent <=> 0 practice — with zero practice fixtures renderL must
|
|
590
|
+
render NO .prac-row; a phantom row aborts the drive, sel stays
|
|
591
|
+
unmatched, the row reads MISSING;
|
|
592
|
+
(2) with 2 mockRun fixtures, the REAL window.__pracExpand must render a
|
|
593
|
+
row whose claimed data-verify-practice-n=2 AND exactly 2 rows inside
|
|
594
|
+
.practice-rows (sel pins both via :nth-child(2):last-child) — a
|
|
595
|
+
"practice runs · 2" label over 0 rows, or practice flooding back
|
|
596
|
+
into the real list, reads MISSING. State restored in undrive. */
|
|
597
|
+
{ id:"F-PRACTICE-ROW", name:"Practice-runs group · quiet collapsed row, claimed N == rendered rows, absent when no practice exists (S18)", surface:"list", sel:"#task-list .prac-row[data-verify-practice-n='2'][aria-expanded='true'] + .practice-rows > .task-card:nth-child(2):last-child", status:"shipped", tier:"v1",
|
|
598
|
+
drive:function(w,d){ try{ w.__covPRAC=w._cached.tasks; w.__pracCollapse(); w._cached.tasks={spawned:[],persisted:[],sessions:[]}; w.renderL(); if(d.querySelector("#task-list .prac-row")) return; var base=Date.now()-3*24*3600*1000; var mkp=function(id,ts){return{sessionId:id,status:"completed",cwd:"/repo/cov-prac",startedAt:ts,prompt:"coverage practice fixture "+id,agent:"claude-code",steps:[],mockRun:true,provenance:"persisted",durationMs:1000,completedAt:ts+1000};}; w._cached.tasks={spawned:[],persisted:[mkp("cov-prac-a",base),mkp("cov-prac-b",base-3600*1000)],sessions:[]}; w.__pracExpand(); }catch(e){} },
|
|
599
|
+
undrive:function(w,d){ try{ w._cached.tasks=w.__covPRAC; w.__pracCollapse(); w.renderL(); }catch(e){} } },
|
|
600
|
+
/* S18 · H13's aggregation path, pinned. Under CODEHERE_MOCK every live run
|
|
601
|
+
is practice, so the board's hero compare is honestly 0==0 — vacuous.
|
|
602
|
+
This drive injects deterministic NON-practice fixtures (one running ·
|
|
603
|
+
one completed-verified · one caught, three distinct agents, started
|
|
604
|
+
today) through the REAL upHero(); sel pins every claimed
|
|
605
|
+
data-verify-hero-* attr to the fixture-derived counts. A double-counted
|
|
606
|
+
agent or mis-bucketed running session reads MISSING. */
|
|
607
|
+
{ id:"F-HERO-COUNTS", name:"Day-line hero · real aggregation path pinned (agents=3 · verified=1 · caught=1 · stopped=0 · running=1) (S18)", surface:"list", sel:"[data-verify-unit='home-hero'][data-verify-hero-mode='counts'][data-verify-hero-agents='3'][data-verify-hero-verified='1'][data-verify-hero-caught='1'][data-verify-hero-stopped='0'][data-verify-hero-running='1']", status:"shipped", tier:"v1",
|
|
608
|
+
drive:function(w,d){ try{ w.__covHERO=w._cached.tasks; var hnow=Date.now(); var mkh=function(id,ag,st,vd){ var t={sessionId:id,status:st,cwd:"/repo/cov-hero",startedAt:hnow,prompt:"coverage hero fixture "+id,agent:ag,steps:[],provenance:"persisted"}; if(vd){ t.verdict={claim:{status:vd}}; t.durationMs=1000; t.completedAt=hnow; } return t; }; w._cached.tasks={spawned:[],persisted:[mkh("cov-hero-run","claude-code","running",null),mkh("cov-hero-ok","codex","completed","verified"),mkh("cov-hero-bad","gemini","completed","broken")],sessions:[]}; w.upHero(); }catch(e){} },
|
|
609
|
+
undrive:function(w,d){ try{ w._cached.tasks=w.__covHERO; w.upHero(); }catch(e){} } },
|
|
308
610
|
/* room selectors are scoped to #task-list: the hidden detail view's DOM
|
|
309
611
|
persists (display:none still matches querySelectorAll) and also has an
|
|
310
612
|
.audit-line — unscoped, a dead room would false-OBSERVE off it. */
|
|
311
613
|
{ id:"F-ROOM-HEAD", name:"Goal room header (title + outcome sub)", surface:"room", sel:"#task-list .room-head .room-title", status:"shipped", tier:"v1" },
|
|
312
|
-
|
|
614
|
+
// F-ROOM-BACK removed 2026-07-07 — the '← Projects' back-link died for the
|
|
615
|
+
// rail's Today row (nav option B); the rail + Esc + the logo are the way out.
|
|
313
616
|
{ id:"F-ROOM-TURNS", name:"Turns in order (how agents worked the goal)", surface:"room", sel:"#task-list .turns-label", status:"partial", tier:"v1" },
|
|
314
617
|
{ id:"F-ROOM-AUDIT", name:"Room audit line (steps in local chain)", surface:"room", sel:"#task-list .audit-line", status:"partial", tier:"v1" },
|
|
315
618
|
/* partial by nature: renders ONLY when the daemon reports project memory
|
|
316
619
|
OFF for the opened room (initialized projects correctly show nothing). */
|
|
317
620
|
{ id:"F-ROOM-MEM-NUDGE", name:"Project-memory init nudge (turn on memory)", surface:"room", sel:"#task-list .mem-nudge", status:"partial", tier:"v1" },
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
621
|
+
/* on-demand surfaces: hidden until a state class the product itself
|
|
622
|
+
toggles. The harness DRIVES that exact state (same honesty bar as the
|
|
623
|
+
seeded fixtures: state occurs -> surface renders), then restores it.
|
|
624
|
+
Before AUD-05 these were "observed" while display:none — a blind spot. */
|
|
625
|
+
{ id:"M-OVERLAY-LINEAR", name:"Overlay-renderer Linear", surface:"list", sel:".or-overlay", status:"shipped", tier:"v0.9",
|
|
626
|
+
drive:function(w,d){ d.getElementById("or-overlay").classList.add("open"); },
|
|
627
|
+
undrive:function(w,d){ d.getElementById("or-overlay").classList.remove("open"); } },
|
|
628
|
+
{ id:"M-PRIMITIVES-NOTE", name:"Primitives note", surface:"list", sel:".primitives-note .prims .p", status:"shipped", tier:"v0.8",
|
|
629
|
+
drive:function(w,d){ w.openReshape(); },
|
|
630
|
+
undrive:function(w,d){ var m=d.getElementById("reshape-modal"); if(m) m.classList.remove("open"); } },
|
|
631
|
+
{ id:"M-RESHAPE-AGENT-COMPOSE", name:"Agent-compose animation", surface:"list", sel:".agent-compose .line", status:"shipped", tier:"v0.8",
|
|
632
|
+
drive:function(w,d){ w.openReshape(); d.getElementById("agent-compose").classList.add("show"); },
|
|
633
|
+
undrive:function(w,d){ d.getElementById("agent-compose").classList.remove("show"); var m=d.getElementById("reshape-modal"); if(m) m.classList.remove("open"); } },
|
|
321
634
|
{ id:"M-SETTINGS-DRAWER", name:"Settings drawer", surface:"list", sel:".drawer", status:"shipped", tier:"v0.8" },
|
|
322
635
|
{ id:"M-SHARE-TOAST", name:"Share toast notification", surface:"list", sel:".share-toast", status:"shipped", tier:"v0.8" },
|
|
323
|
-
{ id:"M-WPASS-APPROVAL", name:"W-PASS approval modal", surface:"list", sel:".wp-modal", status:"shipped", tier:"v0.9"
|
|
324
|
-
|
|
325
|
-
|
|
636
|
+
{ id:"M-WPASS-APPROVAL", name:"W-PASS approval modal", surface:"list", sel:".wp-modal", status:"shipped", tier:"v0.9",
|
|
637
|
+
drive:function(w,d){ d.getElementById("wp-modal").classList.add("open"); },
|
|
638
|
+
undrive:function(w,d){ d.getElementById("wp-modal").classList.remove("open"); } },
|
|
639
|
+
{ id:"P-AB-BADGE", name:"Agent identity badge (S13: on history cards + detail; rows carry the mono name)", surface:"list", sel:".ab", status:"shipped", tier:"v0.6",
|
|
640
|
+
drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
|
|
641
|
+
undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
|
|
642
|
+
{ id:"P-STATUS-PILL", name:"Per-status pill on the card (.tc-stat: running pulse / verified / reported / caught)", surface:"list", sel:".tcard .tc-stat", status:"shipped", tier:"v0.7",
|
|
643
|
+
drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
|
|
644
|
+
undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
|
|
326
645
|
{ id:"F-DETAIL-HEADER", name:"Detail view header (back button + title + agent badge + status line)", surface:"detail", sel:".back-btn", status:"shipped", tier:"v0.7" },
|
|
327
646
|
{ id:"F-DETAIL-KICKER", name:"Detail kicker + failover context", surface:"detail", sel:".detail-kicker", status:"shipped", tier:"v0.7" },
|
|
328
647
|
{ id:"F-OUTCOME-PANEL", name:"Outcome section (files/steps/Final Report)", surface:"detail", sel:"[data-verify-unit='outcome']", status:"shipped", tier:"v0.7" },
|
|
329
648
|
{ id:"F-SHIMMER-TITLE", name:"Animated gradient title", surface:"detail", sel:".detail-title", status:"shipped", tier:"v0.7" },
|
|
330
649
|
{ id:"P-CODE-CARD", name:"Inline syntax-tinted code preview", surface:"detail", sel:".code-card", status:"shipped", tier:"v0.7" },
|
|
331
650
|
{ id:"P-CTX-URL-CHIP", name:"Clickable context-source URL preview chip", surface:"detail", sel:".ctx-url-chip", status:"shipped", tier:"v0.8" },
|
|
332
|
-
|
|
333
|
-
|
|
651
|
+
/* S14 · the celebration card died with the calm redesign — the verified
|
|
652
|
+
state is now the dot+word verdict line (the only green). Honest
|
|
653
|
+
re-point: observe the SAME state (verdict=verified) on its new dress. */
|
|
654
|
+
{ id:"P-VERIFIED-BLOCK", name:"Verified verdict line (S14: dot + word, the only green)", surface:"detail", sel:".vline[data-verify-verdict='verified']", status:"shipped", tier:"v0.9",
|
|
655
|
+
/* the verified verdict line renders in the detail for a completed,
|
|
656
|
+
git-tracked, non-mock task with a verified claim (verdictState==='verified').
|
|
657
|
+
Mock seeds are practice/reported. Mutate the ALREADY-OPEN detail task
|
|
658
|
+
in place (no navigate/no _cached swap — that corrupts the shared detail
|
|
659
|
+
other detail probes observe), re-render, observe, restore. */
|
|
660
|
+
drive:function(w,d){ try{ var t=w.findS?w.findS(COV_FIXTURE):null; if(t){ w.__covVB={v:t.verdict,m:t.mockRun,s:t.status,g:(t.artifacts?t.artifacts.noGitTracking:undefined),r:t.finalReport}; t.verdict={claim:{status:"verified"}}; t.mockRun=false; t.status="completed"; if(t.artifacts)t.artifacts.noGitTracking=false; t.finalReport={summary:"coverage fixture \\u00b7 claims match reality",source:"agent-summary"}; w.renderD(t); } }catch(e){} },
|
|
661
|
+
undrive:function(w,d){ try{ var t=w.findS?w.findS(COV_FIXTURE):null; if(t&&w.__covVB){ t.verdict=w.__covVB.v; t.mockRun=w.__covVB.m; t.status=w.__covVB.s; if(t.artifacts)t.artifacts.noGitTracking=w.__covVB.g; t.finalReport=w.__covVB.r; w.renderD(t); } }catch(e){} } },
|
|
662
|
+
{ id:"P-WRITE-PASS", name:"Write-pass inline card (blue-accented)", surface:"detail", sel:".write-pass .head + h4", status:"shipped", tier:"v0.9",
|
|
663
|
+
drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
|
|
664
|
+
undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
|
|
334
665
|
{ id:"F-RECOVERY-CARD", name:"failed-task recovery card", surface:"detail", sel:".recovery-card", status:"shipped", tier:"v0.7", fixture:"demo-recovery-test-failed-v0.9" },
|
|
335
666
|
{ id:"F-CRASH-RECOVERY", name:"orphaned-session banner", surface:"detail", sel:".crash-recovery", status:"shipped", tier:"v0.7", fixture:"demo-crash-orphaned-mid-write-v0.9" },
|
|
336
667
|
{ id:"F-HOOK-BLOCK-CARD", name:"hook-block card + override", surface:"detail", sel:".hook-block-card", status:"shipped", tier:"v0.7", fixture:"demo-hook-block-bulk-rm-v0.9" },
|
|
337
668
|
{ id:"F-OVERRIDE-FLOW", name:"override + log reason", surface:"detail", sel:".hook-block-actions", status:"shipped", tier:"v0.7", fixture:"demo-hook-block-bulk-rm-v0.9" },
|
|
338
|
-
{ id:"F-GIT-NOTE", name:"non-git honest note", surface:"detail", sel:".git-note,.
|
|
339
|
-
|
|
669
|
+
{ id:"F-GIT-NOTE", name:"non-git honest note (S14: .verifier-panel died · the partial verdict line carries it)", surface:"detail", sel:".git-note,.vline[data-verify-verdict='partial']", status:"shipped", tier:"v0.7", fixture:"demo-non-git-honest-v1" },
|
|
670
|
+
/* S14 · the conversation rests as one collapsed row ("conversation · N
|
|
671
|
+
turns · every tool call recorded"); these rows drive the REAL expand
|
|
672
|
+
(details.conv-wrap.open — the same toggle the user's click flips),
|
|
673
|
+
observe inside, then collapse back. */
|
|
674
|
+
{ id:"F-CONVERSATION", name:"multi-speaker conversation (S14: behind the collapsed conversation row)", surface:"detail", sel:".conv-turn,.turn,.speaker", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9",
|
|
675
|
+
drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
|
|
676
|
+
undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
|
|
340
677
|
{ id:"P-ARTIFACTS-STRUCTURED", name:"structured artifacts", surface:"detail", sel:".art-cmp,.artifact,.art-card,.struct-art", status:"partial", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
|
|
341
678
|
{ id:"P-PANEL-TITLE", name:"uppercase panel title", surface:"detail", sel:".panel-title", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
|
|
342
|
-
{ id:"F-THREAD-PHASED", name:"phased narrative timeline", surface:"detail", sel:".tl-div,.chapter-card,.phase-pos", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9"
|
|
679
|
+
{ id:"F-THREAD-PHASED", name:"phased narrative timeline", surface:"detail", sel:".tl-div,.chapter-card,.phase-pos", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9",
|
|
680
|
+
drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
|
|
681
|
+
undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
|
|
343
682
|
{ id:"F-FILES-TESTS", name:"files & tests panel", surface:"detail", sel:".file-tree", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
|
|
344
|
-
{ id:"P-TOOL-CARD", name:"tool-call activity card", surface:"detail", sel:".tl-acts", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9"
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
{ id:"
|
|
683
|
+
{ id:"P-TOOL-CARD", name:"tool-call activity card", surface:"detail", sel:".tl-acts", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9",
|
|
684
|
+
drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
|
|
685
|
+
undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
|
|
686
|
+
{ id:"F-THINKING-EMITTER", name:"live thinking dots (needs SSE token stream)", surface:"detail", sel:".thinking", status:"partial", tier:"v0.7", fixture:"demo-running-streaming-v0.9",
|
|
687
|
+
drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
|
|
688
|
+
undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
|
|
689
|
+
{ id:"P-REPL-PRIMITIVES", name:"streaming repl-beat (needs SSE token stream)", surface:"detail", sel:".repl-beat", status:"partial", tier:"v0.7", fixture:"demo-running-streaming-v0.9",
|
|
690
|
+
drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
|
|
691
|
+
undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
|
|
692
|
+
{ id:"P-AUDIT-LINE", name:"inline audit beat (host-thread / live audit)", surface:"detail", sel:".audit-line", status:"partial", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9",
|
|
693
|
+
drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
|
|
694
|
+
undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
|
|
348
695
|
{ id:"F-TOKEN-TABLE", name:"per-agent token table (needs artifacts.usage)", surface:"detail", sel:".token-table", status:"partial", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
696
|
+
/* overlay-renderer per-source cards · the drive renders the REAL per-source
|
|
697
|
+
render branch over a fixture payload (same shape the adapter unit fixtures
|
|
698
|
+
produce) so .overlay-figma/.overlay-drawio are observable without a live
|
|
699
|
+
Figma token or network fetch. */
|
|
700
|
+
{ id:"M-OVERLAY-FIGMA", name:"overlay-renderer · Figma", surface:"list", sel:".overlay-figma", status:"shipped", tier:"v1.0",
|
|
701
|
+
drive:function(w,d){ w.codehereRenderOverlayPayload({ source:"figma", url:"figma://file/abc123", text:"Figma file · API Design\\nCANVAS · Mobile\\n FRAME · Home 375×812\\n TEXT · Title · \\"Welcome to the app\\"", metadata:{ fileKey:"abc123", name:"API Design", lastModified:"2026-05-21T09:00:00Z", frames:[{ name:"Home", width:375, height:812, text:"Welcome to the app" }] } }); },
|
|
702
|
+
undrive:function(w,d){ var o=d.getElementById("or-overlay"); if(o) o.classList.remove("open"); } },
|
|
703
|
+
{ id:"M-OVERLAY-DRAWIO", name:"overlay-renderer · draw.io", surface:"list", sel:".overlay-drawio", status:"shipped", tier:"v1.0",
|
|
704
|
+
drive:function(w,d){ w.codehereRenderOverlayPayload({ source:"drawio", url:"drawio://gh/owner/repo/docs/arch.drawio", text:"draw.io diagram · github.com/owner/repo/docs/arch.drawio\\n\\nstructure: 5 cells · 1 edges · 1 page\\n--- raw xml (truncated) ---\\n<mxfile><diagram><mxGraphModel>...", metadata:{ label:"github.com/owner/repo/docs/arch.drawio", bytes:842, cellCount:5, edgeCount:1, pageCount:1 } }); },
|
|
705
|
+
undrive:function(w,d){ var o=d.getElementById("or-overlay"); if(o) o.classList.remove("open"); } },
|
|
706
|
+
{ id:"V1-MULTITASK", name:"multi-task projects · parent/child", surface:"list", sel:".project-rollup", status:"shipped", tier:"v1.0",
|
|
707
|
+
/* It-4 · banner-drive precedent: inject a known 3-task batch STATE
|
|
708
|
+
(__seedCovBatch, never DOM), then the REAL openRoom() -> renderRoom()
|
|
709
|
+
derives the .project-rollup row live. Restored in undrive. */
|
|
710
|
+
drive:function(w,d){ try{ __seedCovBatch(w); w.__covBatchCwd=__openBatchRoom(w); }catch(e){} },
|
|
711
|
+
undrive:function(w,d){ try{ if(w.closeRoom) w.closeRoom(); __unseedCovBatch(w); }catch(e){} } },
|
|
712
|
+
{ id:"V1-FANOUT-ALL", name:"@all fanout to ready adapters", surface:"list", sel:".fanout-all", status:"shipped", tier:"v1.0",
|
|
713
|
+
/* Same injected batch room renders the .fanout-all grid (one column/adapter). */
|
|
714
|
+
drive:function(w,d){ try{ __seedCovBatch(w); __openBatchRoom(w); }catch(e){} },
|
|
715
|
+
undrive:function(w,d){ try{ if(w.closeRoom) w.closeRoom(); __unseedCovBatch(w); }catch(e){} } },
|
|
353
716
|
{ id:"F-AGENT-SWITCHER", name:"'reply as' agent picker", surface:"detail", sel:".agent-switcher", status:"future", tier:"cut" }
|
|
354
717
|
];
|
|
355
718
|
|
|
@@ -364,13 +727,230 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
364
727
|
return { verdict: el.getAttribute("data-verify-verdict"), gittracked: el.getAttribute("data-verify-gittracked") };
|
|
365
728
|
});
|
|
366
729
|
var says = Array.prototype.slice.call(doc.querySelectorAll('.tl-say')).map(function(el){
|
|
367
|
-
|
|
730
|
+
// A glance turn carries its words in .gl-line (the one-sentence glance);
|
|
731
|
+
// a plain reasoning bubble in .tl-say-text. First match in document
|
|
732
|
+
// order wins — an actually-wordless bubble still reads len 0 and FAILS.
|
|
733
|
+
var w = el.querySelector(".tl-say-text, .gl-line");
|
|
734
|
+
var t = (w ? w.textContent : "") || "";
|
|
368
735
|
t = t.replace(/^\\s+|\\s+$/g, "");
|
|
369
736
|
return { len:t.length, text:t };
|
|
370
737
|
});
|
|
738
|
+
// Thariq loop-1 · recovery-card buttons. Capture each action button's
|
|
739
|
+
// enabled state + whether its onclick is a bare al() toast — so the
|
|
740
|
+
// recovery-buttons-must-not-lie probe can FAIL on a relapse.
|
|
741
|
+
var recoveryButtons = Array.prototype.slice.call(doc.querySelectorAll('.recovery-card [data-verify-action]')).map(function(el){
|
|
742
|
+
var oc = el.getAttribute("onclick") || "";
|
|
743
|
+
return {
|
|
744
|
+
action: el.getAttribute("data-verify-action"),
|
|
745
|
+
disabled: el.hasAttribute("disabled"),
|
|
746
|
+
// a "lie" = an ENABLED button whose only behavior is a toast
|
|
747
|
+
bareToast: !el.hasAttribute("disabled") && /^\\s*al\\(/.test(oc),
|
|
748
|
+
hasReason: !!el.getAttribute("title"),
|
|
749
|
+
};
|
|
750
|
+
});
|
|
751
|
+
// Thariq loop-2 · the observer rail. Capture its mode (empty/active), the
|
|
752
|
+
// claimed live count, per-row states, and whether the empty state actually
|
|
753
|
+
// teaches (non-blank) — so H8 can pin: never a stale "live", never a blank
|
|
754
|
+
// empty box, never fake rows.
|
|
755
|
+
var obsRail = doc.querySelector('[data-verify-unit="observer"]');
|
|
756
|
+
var observer = obsRail ? {
|
|
757
|
+
mode: obsRail.getAttribute("data-verify-observer"),
|
|
758
|
+
liveClaimed: Number(obsRail.getAttribute("data-verify-observer-live") || 0),
|
|
759
|
+
rowStates: Array.prototype.slice.call(obsRail.querySelectorAll('[data-verify-obs-state]')).map(function(r){ return r.getAttribute("data-verify-obs-state"); }),
|
|
760
|
+
teachLen: (obsRail.querySelector('.obs-teach') ? (obsRail.querySelector('.obs-teach').textContent||"").trim().length : 0),
|
|
761
|
+
textLen: (obsRail.textContent||"").trim().length
|
|
762
|
+
} : null;
|
|
763
|
+
// S7 · the composer cost chips. Capture each chip's computed mode + what
|
|
764
|
+
// it says, so H10 can pin: the chip never claims a computation that
|
|
765
|
+
// didn't happen (the pre-S7 chip was a hardcoded ~$0.02 under a
|
|
766
|
+
// "based on prompt length + pricing" tooltip).
|
|
767
|
+
var costEst = Array.prototype.slice.call(doc.querySelectorAll('[data-verify-unit="cost-est"]')).map(function(el){
|
|
768
|
+
return {
|
|
769
|
+
mode: el.getAttribute("data-verify-mode"),
|
|
770
|
+
text: (el.textContent||"").trim(),
|
|
771
|
+
title: el.getAttribute("title") || ""
|
|
772
|
+
};
|
|
773
|
+
});
|
|
371
774
|
var found = !!(meta && visible);
|
|
775
|
+
// S12 · H13 — the home hero's CLAIMED counts (stamped data-verify-hero-*)
|
|
776
|
+
// vs an INDEPENDENT re-derivation from the same task state the page holds
|
|
777
|
+
// (_cached via the iframe window). The hero element persists in the DOM
|
|
778
|
+
// while a detail is open, so the check is readable from any fixture.
|
|
779
|
+
var heroEl = doc.querySelector('[data-verify-unit="home-hero"]');
|
|
780
|
+
var hero = null;
|
|
781
|
+
if (heroEl){
|
|
782
|
+
hero = {
|
|
783
|
+
mode: heroEl.getAttribute("data-verify-hero-mode"),
|
|
784
|
+
agents: Number(heroEl.getAttribute("data-verify-hero-agents")||0),
|
|
785
|
+
verified: Number(heroEl.getAttribute("data-verify-hero-verified")||0),
|
|
786
|
+
caught: Number(heroEl.getAttribute("data-verify-hero-caught")||0),
|
|
787
|
+
stopped: Number(heroEl.getAttribute("data-verify-hero-stopped")||0),
|
|
788
|
+
running: Number(heroEl.getAttribute("data-verify-hero-running")||0),
|
|
789
|
+
expected: null
|
|
790
|
+
};
|
|
791
|
+
try {
|
|
792
|
+
var hw = doc.defaultView;
|
|
793
|
+
var hAll = (hw && typeof hw.allS==="function") ? hw.allS((hw._cached && hw._cached.tasks)||{}) : [];
|
|
794
|
+
/* S18 · PAIRED with ui-page-v2 heroCounts' top filter: practice runs
|
|
795
|
+
(mockRun) never feed the day-line. Both sides exclude them
|
|
796
|
+
identically — the honest-rule update, not a weakened probe. */
|
|
797
|
+
var hReal = hAll.filter(function(t){ return !/^demo-/.test(t.sessionId||"") && !t.mockRun; });
|
|
798
|
+
var hSod = new Date(); hSod.setHours(0,0,0,0);
|
|
799
|
+
var hToday = hReal.filter(function(t){ return (t.startedAt||0) >= hSod.getTime(); });
|
|
800
|
+
var hLive = ["running","queued","paused","blocked"];
|
|
801
|
+
// Refuter C2 · this used to copy heroCounts' own (then-broken)
|
|
802
|
+
// expressions — a probe that re-derives the claim pipeline is probe
|
|
803
|
+
// theater. Derive from the VERDICT ground truth: verified = the
|
|
804
|
+
// verdict says claims match reality, on a real (non-mock) completed
|
|
805
|
+
// git-tracked run; caught = the verdict says broken; stopped =
|
|
806
|
+
// terminal-unclean statuses; running ignores the day boundary.
|
|
807
|
+
var hLiveAll = hReal.filter(function(t){ return hLive.indexOf(t.status)>=0; });
|
|
808
|
+
var hAg = {};
|
|
809
|
+
hToday.concat(hLiveAll).forEach(function(t){ var a=String(t.agent||""); if(a) hAg[a]=1; });
|
|
810
|
+
hero.expected = {
|
|
811
|
+
today: hToday.length,
|
|
812
|
+
agents: Object.keys(hAg).length,
|
|
813
|
+
// PAIRED with heroCounts' composite-grade rule (2.2): green needs an
|
|
814
|
+
// INDEPENDENT signal when a grade is attached; the legacy claim check
|
|
815
|
+
// applies only to verdicts without one. The sides drifted when the
|
|
816
|
+
// hero adopted grades and this stayed on claim-status — first exposed
|
|
817
|
+
// 2026-07-02 by a real --ship run (claim verified, self-report only):
|
|
818
|
+
// hero said 1, this derived 2. Change BOTH sides identically.
|
|
819
|
+
verified: hToday.filter(function(t){
|
|
820
|
+
return !!(t.verdict && (t.verdict.grade ? t.verdict.grade.green : (t.verdict.claim && t.verdict.claim.status==="verified")))
|
|
821
|
+
&& !t.mockRun
|
|
822
|
+
&& ["completed","done"].indexOf(t.status)>=0
|
|
823
|
+
&& !(t.artifacts && t.artifacts.noGitTracking);
|
|
824
|
+
}).length,
|
|
825
|
+
caught: hToday.filter(function(t){ var v=t.verdict; return !!(v && (v.grade ? v.grade.headline==="broken" : (v.claim && v.claim.status==="broken"))); }).length,
|
|
826
|
+
stopped: hToday.filter(function(t){ return ["failed","error","crashed","killed"].indexOf(t.status)>=0; }).length,
|
|
827
|
+
running: hLiveAll.length
|
|
828
|
+
};
|
|
829
|
+
} catch(e){ /* state unreachable — H13 reports BLOCKED, never a silent green */ }
|
|
830
|
+
}
|
|
831
|
+
// S13 · H14 — the decision queue's CLAIMED N (data-verify-decisions-n)
|
|
832
|
+
// vs an INDEPENDENT re-derivation from the same state the page holds:
|
|
833
|
+
// pending write-passes (window._wpPendingAll, the list wpPoll feeds the
|
|
834
|
+
// queue AND the modal from) + verifier-broken verdicts today + failed
|
|
835
|
+
// runs with the prompt recorded (Retry available), non-demo, today.
|
|
836
|
+
// Like the hero, the container persists in the DOM while a detail is
|
|
837
|
+
// open, so the check is readable from any fixture.
|
|
838
|
+
var dqEl = doc.querySelector('[data-verify-unit="decisions"]');
|
|
839
|
+
var decisions = {
|
|
840
|
+
present: !!dqEl,
|
|
841
|
+
n: dqEl ? Number(dqEl.getAttribute("data-verify-decisions-n")||0) : 0,
|
|
842
|
+
rows: dqEl ? dqEl.querySelectorAll(".dq-row").length : 0,
|
|
843
|
+
expected: null
|
|
844
|
+
};
|
|
845
|
+
try {
|
|
846
|
+
var dw = doc.defaultView;
|
|
847
|
+
if (dw && typeof dw.allS === "function"){
|
|
848
|
+
/* S18 · PAIRED with ui-page-v2 decisionItems' top filter: a practice
|
|
849
|
+
(mockRun) failure never demands a real decision. Both sides exclude
|
|
850
|
+
them identically — the honest-rule update, not a weakened probe. */
|
|
851
|
+
var dAll = dw.allS((dw._cached && dw._cached.tasks)||{}).filter(function(t){ return !/^demo-/.test(t.sessionId||"") && !t.mockRun; });
|
|
852
|
+
var dSod = new Date(); dSod.setHours(0,0,0,0);
|
|
853
|
+
var dToday = function(t){ return (t.startedAt||0) >= dSod.getTime(); };
|
|
854
|
+
var dBrokenIs = function(t){ return !!(t.verdict && t.verdict.claim && t.verdict.claim.status==="broken"); };
|
|
855
|
+
var dPasses = (dw._wpPendingAll||[]).length;
|
|
856
|
+
var dBroken = dAll.filter(function(t){ return dToday(t) && dBrokenIs(t); }).length;
|
|
857
|
+
var dFailed = dAll.filter(function(t){ return dToday(t) && ["failed","error","crashed","killed"].indexOf(t.status)>=0 && t.prompt && !dBrokenIs(t); }).length;
|
|
858
|
+
decisions.expected = dPasses + dBroken + dFailed;
|
|
859
|
+
}
|
|
860
|
+
} catch(e){ /* state unreachable — H14 reports BLOCKED, never a silent green */ }
|
|
861
|
+
// S12 · the detail verdict hero stamps which staging it chose; it ALSO
|
|
862
|
+
// carries data-verify-unit="verifier" so H1/H9 already read it above.
|
|
863
|
+
var vhEl = doc.querySelector("[data-verify-verdict-hero]");
|
|
864
|
+
var verdictHero = vhEl ? vhEl.getAttribute("data-verify-verdict-hero") : null;
|
|
865
|
+
// S2 · a running task must carry a Stop control on its detail.
|
|
866
|
+
var hasStopControl = !!doc.querySelector('.detail-meta [data-verify-action="stop"]');
|
|
867
|
+
// S1 · the detail composer's follow-up honesty: enabled means the server
|
|
868
|
+
// says the message can be DELIVERED; disabled must carry a plain reason.
|
|
869
|
+
var dta = doc.getElementById('detail-ta');
|
|
870
|
+
var followUp = dta ? {
|
|
871
|
+
state: dta.getAttribute('data-verify-followup'),
|
|
872
|
+
disabled: dta.disabled,
|
|
873
|
+
placeholderLen: (dta.getAttribute('placeholder')||'').trim().length
|
|
874
|
+
} : null;
|
|
875
|
+
// S19 · glyph discipline — scan the REAL rendered surface (visible text
|
|
876
|
+
// nodes + human-facing attributes, script/style excluded) for emoji /
|
|
877
|
+
// pictograph codepoints outside the sanctioned geometric glyph set
|
|
878
|
+
// (\\u2713 \\u2715 \\u25CF \\u25CB \\u2022 \\u2298 \\u203A \\u26A0 \\u2014 \\u00B7, \\u2191 send, \\u2190 back,
|
|
879
|
+
// \\u2318/\\u23CE keyboard hints). Effect-observing: a re-introduced emoji
|
|
880
|
+
// anywhere in the driven chrome FAILS the board via H15.
|
|
881
|
+
var glyphScan = { scanned:false, hits:[] };
|
|
882
|
+
try {
|
|
883
|
+
// S19 R2 · the token contract bans emoji-AS-ICON (chrome), never emoji
|
|
884
|
+
// a user typed: [data-user-content] containers (prompts, say/think
|
|
885
|
+
// text, reports, step labels/outputs) are run CONTENT and excluded —
|
|
886
|
+
// an honest "fix the bug" prompt with an emoji must not void the board.
|
|
887
|
+
var gIsContent = function(el){
|
|
888
|
+
while (el && el.nodeType===1){
|
|
889
|
+
if (el.hasAttribute && el.hasAttribute("data-user-content")) return true;
|
|
890
|
+
el = el.parentNode;
|
|
891
|
+
}
|
|
892
|
+
return false;
|
|
893
|
+
};
|
|
894
|
+
var gParts = [];
|
|
895
|
+
var gCollect = function(){
|
|
896
|
+
var gWalk = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, { acceptNode: function(nd){
|
|
897
|
+
var pn = nd.parentNode && nd.parentNode.nodeName;
|
|
898
|
+
if (pn==="SCRIPT"||pn==="STYLE") return NodeFilter.FILTER_REJECT;
|
|
899
|
+
return gIsContent(nd.parentNode) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
|
|
900
|
+
} });
|
|
901
|
+
while (gWalk.nextNode()) gParts.push(gWalk.currentNode.nodeValue);
|
|
902
|
+
Array.prototype.slice.call(doc.body.querySelectorAll("*")).forEach(function(el){
|
|
903
|
+
if (el.nodeName==="SCRIPT"||el.nodeName==="STYLE") return;
|
|
904
|
+
if (gIsContent(el)) return;
|
|
905
|
+
["title","placeholder","aria-label","alt","value"].forEach(function(at){
|
|
906
|
+
var v = el.getAttribute && el.getAttribute(at);
|
|
907
|
+
if (v) gParts.push(v);
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
};
|
|
911
|
+
gCollect();
|
|
912
|
+
// S19 R2 · the chrome claim must be OBSERVED in the one-click-away
|
|
913
|
+
// collapse states too: drive the REAL history/practice toggles (the
|
|
914
|
+
// same render path the S18 coverage drives use), collect again, then
|
|
915
|
+
// restore whichever state the surface was in. Pure UI state — no data
|
|
916
|
+
// is changed, and the duplicate text just re-feeds the dedup'd regex.
|
|
917
|
+
var gw = doc.defaultView;
|
|
918
|
+
var gHistBtn = doc.querySelector(".hist-row:not(.prac-row)");
|
|
919
|
+
var gPracBtn = doc.querySelector(".prac-row");
|
|
920
|
+
var gHistWas = gHistBtn ? gHistBtn.getAttribute("aria-expanded")==="true" : false;
|
|
921
|
+
var gPracWas = gPracBtn ? gPracBtn.getAttribute("aria-expanded")==="true" : false;
|
|
922
|
+
try {
|
|
923
|
+
if (gw && gw.__histExpand) gw.__histExpand();
|
|
924
|
+
if (gw && gw.__pracExpand) gw.__pracExpand();
|
|
925
|
+
gCollect();
|
|
926
|
+
} finally {
|
|
927
|
+
try { if (gw && !gHistWas && gw.__histCollapse) gw.__histCollapse(); } catch(e2){}
|
|
928
|
+
try { if (gw && !gPracWas && gw.__pracCollapse) gw.__pracCollapse(); } catch(e2){}
|
|
929
|
+
}
|
|
930
|
+
var gText = gParts.join("\\n");
|
|
931
|
+
var gRe = /[\\u2192-\\u21FF]|[\\u2300-\\u2317]|[\\u2319-\\u23CD]|[\\u23CF-\\u23FF]|[\\u2600-\\u269F]|[\\u26A1-\\u26FF]|[\\u2700-\\u2712]|\\u2714|[\\u2716-\\u27BF]|[\\u2B00-\\u2BFF]|\\uFE0F|[\\uD83A-\\uD83F][\\uDC00-\\uDFFF]|[\\uD83C-\\uD83E][\\uDC00-\\uDFFF]/g;
|
|
932
|
+
var gm, gSeen = {};
|
|
933
|
+
while ((gm = gRe.exec(gText))){
|
|
934
|
+
var gc = gm[0];
|
|
935
|
+
if (gSeen[gc]) continue;
|
|
936
|
+
gSeen[gc] = 1;
|
|
937
|
+
var gcp = gc.codePointAt(0).toString(16).toUpperCase();
|
|
938
|
+
glyphScan.hits.push("U+"+gcp+" ('"+gc+"') rendered in the chrome \\u00b7 emoji/pictograph outside the sanctioned glyph set");
|
|
939
|
+
}
|
|
940
|
+
glyphScan.scanned = true;
|
|
941
|
+
} catch(e){ /* unscannable -> H15 reports BLOCKED, never a silent green */ }
|
|
372
942
|
return {
|
|
943
|
+
glyphScan: glyphScan,
|
|
373
944
|
found: found,
|
|
945
|
+
hero: hero,
|
|
946
|
+
decisions: decisions,
|
|
947
|
+
verdictHero: verdictHero,
|
|
948
|
+
observer: observer,
|
|
949
|
+
costEst: costEst,
|
|
950
|
+
hasStopControl: hasStopControl,
|
|
951
|
+
followUp: followUp,
|
|
952
|
+
recoveryButtons: recoveryButtons,
|
|
953
|
+
recoveryState: (doc.querySelector('.recovery-card') ? doc.querySelector('.recovery-card').getAttribute('data-verify-recovery') : null),
|
|
374
954
|
status: meta ? meta.getAttribute("data-verify-status") : null,
|
|
375
955
|
terminalFail: meta ? meta.getAttribute("data-verify-terminal-fail")==="true" : false,
|
|
376
956
|
hasExplanation: meta ? meta.getAttribute("data-verify-has-explanation")==="true" : false,
|
|
@@ -402,9 +982,18 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
402
982
|
tries++;
|
|
403
983
|
try {
|
|
404
984
|
var w = frameWin(), d = frameDoc();
|
|
405
|
-
|
|
985
|
+
/* S13 · at rest the home may legitimately have NO task-cards (today
|
|
986
|
+
empty -> first-run hero; demos collapsed behind the history row).
|
|
987
|
+
Ready = task state loaded AND renderL produced a real at-rest
|
|
988
|
+
surface (rows, observer, the history row, or the empty state). */
|
|
989
|
+
if (w && typeof w.showD === "function" && w._cached && w._cached.tasks && d && d.querySelectorAll(".task-card, .obs-row, .hist-row, .empty-state").length > 0) return resolve();
|
|
990
|
+
// The dashboard auto-opens a running task's detail on load, which
|
|
991
|
+
// hides the list and left runAll dead-on-arrival ("surface never
|
|
992
|
+
// became ready") whenever anything was running — found cold by the
|
|
993
|
+
// S5-S8 acceptance walk. Close the auto-opened detail and re-poll.
|
|
994
|
+
if (w && typeof w.hideD === "function" && d && d.body && d.body.classList.contains("detail-open")) { try { w.hideD(); } catch(e2){} }
|
|
406
995
|
} catch(e){ /* not loaded yet */ }
|
|
407
|
-
if (tries > 80) return reject(new Error("surface never became ready (no showD / no task cards)
|
|
996
|
+
if (tries > 80) return reject(new Error("surface never became ready (no showD / no task cards) · is the daemon serving seeded sessions?"));
|
|
408
997
|
setTimeout(poll, 150);
|
|
409
998
|
})();
|
|
410
999
|
});
|
|
@@ -412,11 +1001,23 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
412
1001
|
|
|
413
1002
|
// Navigate the REAL surface to a session and read its contract. Honest about
|
|
414
1003
|
// the not-seeded case: returns found=false rather than inventing a result.
|
|
415
|
-
|
|
1004
|
+
// S20 · a fixture may carry { sid, lens }: the lens is applied through the
|
|
1005
|
+
// page's OWN applyLens (the same function the ⌘K command runs — a real
|
|
1006
|
+
// recomposition, never harness CSS), observed, then restored to default.
|
|
1007
|
+
function observe(fx){
|
|
1008
|
+
var sid = (fx && typeof fx === "object") ? (fx.sid || fx.id) : fx;
|
|
1009
|
+
var lens = (fx && typeof fx === "object") ? fx.lens : null;
|
|
416
1010
|
var w = frameWin(), d = frameDoc();
|
|
417
1011
|
try { if (typeof w.hideD === "function") w.hideD(); } catch(e){}
|
|
418
|
-
try {
|
|
419
|
-
|
|
1012
|
+
try {
|
|
1013
|
+
if (lens && typeof w.applyLens === "function") w.applyLens(lens);
|
|
1014
|
+
w.showD(sid);
|
|
1015
|
+
} catch(e){ return Promise.resolve({ found:false, error:String(e), verifiers:[], says:[] }); }
|
|
1016
|
+
return sleep(140).then(function(){
|
|
1017
|
+
var s = readSurface(frameDoc());
|
|
1018
|
+
if (lens){ try { var w2 = frameWin(); if (typeof w2.applyLens === "function") w2.applyLens("default"); } catch(e){} }
|
|
1019
|
+
return s;
|
|
1020
|
+
});
|
|
420
1021
|
}
|
|
421
1022
|
|
|
422
1023
|
/* -------- run one (invariant × fixture) into a structured result -------- */
|
|
@@ -449,7 +1050,7 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
449
1050
|
var tbody = document.querySelector("#matrix tbody");
|
|
450
1051
|
var hrow = "<tr><th style=\\"width:40%\\">Invariant (unit)</th><th class=\\"needs\\" style=\\"width:13%\\">needs</th>";
|
|
451
1052
|
FIXTURES.forEach(function(fx){
|
|
452
|
-
hrow += "<th><div class=\\"fixhdr"+(fx.probe?" probe":"")+"\\">"+(fx.probe?"
|
|
1053
|
+
hrow += "<th><div class=\\"fixhdr"+(fx.probe?" probe":"")+"\\">"+(fx.probe?"probe \\u00b7 ":"")+fx.label+"</div></th>";
|
|
453
1054
|
});
|
|
454
1055
|
hrow += "</tr>";
|
|
455
1056
|
thead.innerHTML = hrow;
|
|
@@ -504,17 +1105,17 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
504
1105
|
|
|
505
1106
|
function paintTally(c){
|
|
506
1107
|
document.getElementById("tally").innerHTML =
|
|
507
|
-
"<span class=\\"p\\">\\
|
|
1108
|
+
"<span class=\\"p\\">\\u2713 "+c.pass+"</span><span class=\\"f\\">\\u2715 "+c.fail+"</span><span class=\\"b\\">\\u2298 "+c.blocked+"</span><span class=\\"t\\">/ "+c.total+"</span>";
|
|
508
1109
|
var line = document.getElementById("verdict-line");
|
|
509
1110
|
if (c.ok){
|
|
510
|
-
line.className = "verdict-line green";
|
|
1111
|
+
line.className = "verdict-line green"; line.setAttribute("data-evidence","measured");
|
|
511
1112
|
line.textContent = "\\u2713 honest: every PASS was observed, and the probe FAILED exactly as designed (the harness can see a lie).";
|
|
512
1113
|
} else {
|
|
513
|
-
line.className = "verdict-line red";
|
|
1114
|
+
line.className = "verdict-line red"; line.setAttribute("data-evidence","measured");
|
|
514
1115
|
var parts = [];
|
|
515
1116
|
if (c.unexpectedFail.length) parts.push("unexpected FAIL: "+c.unexpectedFail.join(", "));
|
|
516
|
-
if (c.probeMissed.length) parts.push("PROBE DID NOT FAIL ("+c.probeMissed.join(", ")+") \\
|
|
517
|
-
line.textContent = "\\
|
|
1117
|
+
if (c.probeMissed.length) parts.push("PROBE DID NOT FAIL ("+c.probeMissed.join(", ")+") \\u00b7 the harness is blind, treat the whole board as void");
|
|
1118
|
+
line.textContent = "\\u2715 " + parts.join(" \\u00b7 ");
|
|
518
1119
|
}
|
|
519
1120
|
}
|
|
520
1121
|
|
|
@@ -527,15 +1128,35 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
527
1128
|
});
|
|
528
1129
|
}
|
|
529
1130
|
|
|
1131
|
+
// S20 · the board observes the DEFAULT lens unless a fixture says otherwise:
|
|
1132
|
+
// pin it before driving (the iframe shares localStorage with the dashboard,
|
|
1133
|
+
// so a user's saved lens would silently reshape every fixture) and restore
|
|
1134
|
+
// the user's saved lens after.
|
|
1135
|
+
function pinLensDefault(){
|
|
1136
|
+
try {
|
|
1137
|
+
var w = frameWin(), d = frameDoc();
|
|
1138
|
+
if (window.__verifyLensSaved === undefined) window.__verifyLensSaved = (d.body.getAttribute("data-lens")) || "default";
|
|
1139
|
+
if (typeof w.applyLens === "function") w.applyLens("default");
|
|
1140
|
+
} catch(e){}
|
|
1141
|
+
}
|
|
1142
|
+
function restoreLens(){
|
|
1143
|
+
try {
|
|
1144
|
+
var w = frameWin();
|
|
1145
|
+
if (typeof w.applyLens === "function") w.applyLens(window.__verifyLensSaved || "default");
|
|
1146
|
+
delete window.__verifyLensSaved;
|
|
1147
|
+
} catch(e){}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
530
1150
|
function runAll(){
|
|
531
1151
|
var btn = document.getElementById("run-all");
|
|
532
1152
|
btn.disabled = true; btn.textContent = "Running\\u2026";
|
|
533
1153
|
var results = [];
|
|
534
1154
|
return surfaceReady().then(function(){
|
|
1155
|
+
pinLensDefault();
|
|
535
1156
|
// iterate fixtures, observe each once, run all invariants against it
|
|
536
1157
|
return FIXTURES.reduce(function(p, fx){
|
|
537
1158
|
return p.then(function(){
|
|
538
|
-
return observe(fx
|
|
1159
|
+
return observe(fx).then(function(s){
|
|
539
1160
|
INVARIANTS.forEach(function(inv){
|
|
540
1161
|
var r = runOne(inv, fx, s);
|
|
541
1162
|
resultMap[key(inv.id, fx.id)] = r;
|
|
@@ -546,6 +1167,7 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
546
1167
|
});
|
|
547
1168
|
}, Promise.resolve());
|
|
548
1169
|
}).then(function(){
|
|
1170
|
+
restoreLens();
|
|
549
1171
|
var c = classify(results);
|
|
550
1172
|
paintTally(c);
|
|
551
1173
|
lastSingle = results[results.length-1] || null;
|
|
@@ -554,9 +1176,10 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
554
1176
|
btn.disabled = false; btn.textContent = "\\u25b6 Run all";
|
|
555
1177
|
return results;
|
|
556
1178
|
}).catch(function(err){
|
|
1179
|
+
restoreLens();
|
|
557
1180
|
btn.disabled = false; btn.textContent = "\\u25b6 Run all";
|
|
558
1181
|
document.getElementById("verdict-line").className = "verdict-line red";
|
|
559
|
-
document.getElementById("verdict-line").textContent = "\\
|
|
1182
|
+
document.getElementById("verdict-line").textContent = "\\u2298 BLOCKED: "+String(err && err.message || err);
|
|
560
1183
|
throw err;
|
|
561
1184
|
});
|
|
562
1185
|
}
|
|
@@ -588,7 +1211,7 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
588
1211
|
var tb = document.querySelector("#cov-matrix tbody");
|
|
589
1212
|
if (!tb) return;
|
|
590
1213
|
tb.innerHTML = FEATURES.map(function(f){
|
|
591
|
-
var statusTag = f.status==="shipped"?"":f.status==="partial"?"<span class=\\"probe-tag\\" style=\\"border-color:rgba(
|
|
1214
|
+
var statusTag = f.status==="shipped"?"":f.status==="partial"?"<span class=\\"probe-tag\\" style=\\"border-color:rgba(217,164,65,.5);color:var(--warn)\\">partial</span>":f.status==="roadmap"?"<span class=\\"probe-tag\\" style=\\"border-color:var(--border);color:var(--fg-40)\\">roadmap</span>":"<span class=\\"probe-tag\\" style=\\"border-color:var(--border);color:var(--fg-40)\\">"+(f.tier==="cut"?"cut":"future")+"</span>";
|
|
592
1215
|
return "<tr><td><span class=\\"unit-id\\">"+f.id+"</span> <span class=\\"unit-desc\\">"+f.name+"</span>"+statusTag+"</td>"+
|
|
593
1216
|
"<td class=\\"needs\\">"+f.tier+"</td>"+
|
|
594
1217
|
"<td class=\\"needs\\">"+f.surface+"</td>"+
|
|
@@ -622,31 +1245,70 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
622
1245
|
var line = document.getElementById("cov-verdict");
|
|
623
1246
|
if (!line) return;
|
|
624
1247
|
if (c.ok){
|
|
625
|
-
line.className = "verdict-line green";
|
|
1248
|
+
line.className = "verdict-line green"; line.setAttribute("data-evidence","measured");
|
|
626
1249
|
line.textContent = "\\u2713 every feature claimed shipped was OBSERVED on the real surface · "+c.shipped+" shipped checked · "+(c.roadmap+c.future)+" correctly absent (roadmap/future).";
|
|
627
1250
|
} else {
|
|
628
|
-
line.className = "verdict-line red";
|
|
629
|
-
line.textContent = "\\
|
|
1251
|
+
line.className = "verdict-line red"; line.setAttribute("data-evidence","measured");
|
|
1252
|
+
line.textContent = "\\u2715 "+c.missing+" feature(s) claimed shipped but NOT rendered: "+c.missingIds.join(", ")+" · PRODUCT_STATE overclaims, fix the doc or the code.";
|
|
630
1253
|
}
|
|
631
1254
|
}
|
|
632
1255
|
|
|
1256
|
+
// OBSERVED requires a VISIBLE element, not just a DOM match — display:none
|
|
1257
|
+
// matched querySelectorAll and green-lit four dead state banners for weeks
|
|
1258
|
+
// (2026-06-07 audit, AUD-05). NOTE: layout-based checks (getClientRects /
|
|
1259
|
+
// offsetParent) cannot work here — the observed iframe sits inside the
|
|
1260
|
+
// harness's own hidden replay panel, so the product surface never has
|
|
1261
|
+
// layout. Instead walk computed styles WITHIN the iframe document: the
|
|
1262
|
+
// product hiding its own element is what we test; harness chrome isn't.
|
|
1263
|
+
function visibleInDoc(el, d){
|
|
1264
|
+
var node = el;
|
|
1265
|
+
while (node && node !== d.documentElement) {
|
|
1266
|
+
var cs = (d.defaultView || window).getComputedStyle(node);
|
|
1267
|
+
if (cs.display === "none" || cs.visibility === "hidden") return false;
|
|
1268
|
+
node = node.parentElement;
|
|
1269
|
+
}
|
|
1270
|
+
return true;
|
|
1271
|
+
}
|
|
1272
|
+
function anyVisible(d, sel){
|
|
1273
|
+
var els = d.querySelectorAll(sel);
|
|
1274
|
+
for (var i = 0; i < els.length; i++) {
|
|
1275
|
+
if (visibleInDoc(els[i], d)) return true;
|
|
1276
|
+
}
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
633
1280
|
function runCoverage(){
|
|
634
1281
|
var btn = document.getElementById("run-cov");
|
|
635
1282
|
if (btn){ btn.disabled = true; btn.textContent = "Running\\u2026"; }
|
|
636
1283
|
covResults = [];
|
|
637
1284
|
return surfaceReady().then(function(){
|
|
638
1285
|
var w = frameWin();
|
|
1286
|
+
pinLensDefault();
|
|
639
1287
|
try { if (typeof w.hideD==="function") w.hideD(); } catch(e){}
|
|
640
1288
|
return sleep(140);
|
|
641
1289
|
}).then(function(){
|
|
642
1290
|
// list-surface features (dashboard visible)
|
|
643
1291
|
var d = frameDoc();
|
|
644
|
-
FEATURES.filter(function(f){ return f.surface==="list" && f.status!=="roadmap" && f.status!=="future"; })
|
|
645
|
-
|
|
1292
|
+
var listFeats = FEATURES.filter(function(f){ return f.surface==="list" && f.status!=="roadmap" && f.status!=="future"; });
|
|
1293
|
+
listFeats.filter(function(f){ return !f.drive; }).forEach(function(f){
|
|
1294
|
+
var present = anyVisible(d, f.sel);
|
|
646
1295
|
var r = { id:f.id, present:present, verdict:covVerdict(f, present) };
|
|
647
1296
|
covResults.push(r); paintCov(r);
|
|
648
1297
|
});
|
|
649
|
-
|
|
1298
|
+
// driven on-demand surfaces: enter the state, observe, restore.
|
|
1299
|
+
return listFeats.filter(function(f){ return !!f.drive; }).reduce(function(p, f){
|
|
1300
|
+
return p.then(function(){
|
|
1301
|
+
var w = frameWin(), dd = frameDoc();
|
|
1302
|
+
try { f.drive(w, dd); } catch(e){}
|
|
1303
|
+
return sleep(120).then(function(){
|
|
1304
|
+
var present = anyVisible(frameDoc(), f.sel);
|
|
1305
|
+
var r = { id:f.id, present:present, verdict:covVerdict(f, present) };
|
|
1306
|
+
covResults.push(r); paintCov(r);
|
|
1307
|
+
try { f.undrive(w, frameDoc()); } catch(e){}
|
|
1308
|
+
return sleep(60);
|
|
1309
|
+
});
|
|
1310
|
+
});
|
|
1311
|
+
}, Promise.resolve());
|
|
650
1312
|
}).then(function(){
|
|
651
1313
|
// detail-surface features, grouped by the fixture that TRIGGERS them.
|
|
652
1314
|
// Conditional surfaces (recovery/crash/hook-block/...) only render on the
|
|
@@ -661,9 +1323,16 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
661
1323
|
return sleep(200).then(function(){
|
|
662
1324
|
var d = frameDoc();
|
|
663
1325
|
byFix[fx].forEach(function(f){
|
|
664
|
-
|
|
1326
|
+
/* S14 · detail rows gained drive/undrive support (same contract
|
|
1327
|
+
as list rows): the conversation rests collapsed, so timeline
|
|
1328
|
+
rows drive the REAL expand (the user's click on the row),
|
|
1329
|
+
observe, then restore. Never DOM injection of content. */
|
|
1330
|
+
var wd = frameWin();
|
|
1331
|
+
if (f.drive) { try { f.drive(wd, d); } catch(e){} }
|
|
1332
|
+
var present = anyVisible(d, f.sel);
|
|
665
1333
|
var r = { id:f.id, present:present, verdict:covVerdict(f, present), fixture:fx };
|
|
666
1334
|
covResults.push(r); paintCov(r);
|
|
1335
|
+
if (f.undrive) { try { f.undrive(wd, d); } catch(e){} }
|
|
667
1336
|
});
|
|
668
1337
|
});
|
|
669
1338
|
});
|
|
@@ -677,20 +1346,31 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
677
1346
|
var room = FEATURES.filter(function(f){ return f.surface==="room" && f.status!=="roadmap" && f.status!=="future"; });
|
|
678
1347
|
if (!room.length) return;
|
|
679
1348
|
var w0 = frameWin();
|
|
1349
|
+
/* S13 · goal-heads sit inside the EXPANDED history now — expand through
|
|
1350
|
+
the real toggle first. If the home has no earlier real tasks at all,
|
|
1351
|
+
fall back to the seeded coverage batch (same precedent as
|
|
1352
|
+
V1-MULTITASK) so the room render path is still observed for real. */
|
|
1353
|
+
try { if (w0.__histExpand) w0.__histExpand(); } catch(e){}
|
|
680
1354
|
var gh = frameDoc().querySelector(".goal-head[data-goal-cwd]");
|
|
681
1355
|
var check = function(){
|
|
682
1356
|
var d = frameDoc();
|
|
683
1357
|
room.forEach(function(f){
|
|
684
|
-
var present = d
|
|
1358
|
+
var present = anyVisible(d, f.sel);
|
|
685
1359
|
var r = { id:f.id, present:present, verdict:covVerdict(f, present) };
|
|
686
1360
|
covResults.push(r); paintCov(r);
|
|
687
1361
|
});
|
|
688
1362
|
};
|
|
689
|
-
|
|
690
|
-
try { w0.openRoom(gh.getAttribute("data-goal-cwd")); } catch(e){}
|
|
1363
|
+
var seededRoom = false;
|
|
1364
|
+
if (gh){ try { w0.openRoom(gh.getAttribute("data-goal-cwd")); } catch(e){} }
|
|
1365
|
+
else {
|
|
1366
|
+
try { if (__seedCovBatch(w0) && __openBatchRoom(w0)) seededRoom = true; } catch(e){}
|
|
1367
|
+
if (!seededRoom){ check(); try{ if(w0.__histCollapse) w0.__histCollapse(); }catch(e){} return; }
|
|
1368
|
+
}
|
|
691
1369
|
return sleep(200).then(function(){
|
|
692
1370
|
check();
|
|
693
1371
|
try { if (w0.closeRoom) w0.closeRoom(); } catch(e){}
|
|
1372
|
+
try { if (seededRoom) __unseedCovBatch(w0); } catch(e){}
|
|
1373
|
+
try { if (w0.__histCollapse) w0.__histCollapse(); } catch(e){}
|
|
694
1374
|
return sleep(120);
|
|
695
1375
|
});
|
|
696
1376
|
}).then(function(){
|
|
@@ -699,12 +1379,14 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
699
1379
|
var r = { id:f.id, present:false, verdict:covVerdict(f, false) };
|
|
700
1380
|
covResults.push(r); paintCov(r);
|
|
701
1381
|
});
|
|
1382
|
+
restoreLens();
|
|
702
1383
|
var c = classifyCov(covResults);
|
|
703
1384
|
paintCovTally(c);
|
|
704
1385
|
window.__verify_coverage = { results:covResults, summary:c };
|
|
705
1386
|
if (btn){ btn.disabled = false; btn.textContent = "\\u25b6 Run coverage"; }
|
|
706
1387
|
return covResults;
|
|
707
1388
|
}).catch(function(err){
|
|
1389
|
+
restoreLens();
|
|
708
1390
|
if (btn){ btn.disabled = false; btn.textContent = "\\u25b6 Run coverage"; }
|
|
709
1391
|
throw err;
|
|
710
1392
|
});
|
|
@@ -737,7 +1419,7 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
737
1419
|
function openReplay(){ replayOpen = true; document.getElementById("replay").classList.remove("hidden"); buildStepList(); }
|
|
738
1420
|
function buildStepList(){
|
|
739
1421
|
document.getElementById("steplist").innerHTML = FIXTURES.map(function(fx,i){
|
|
740
|
-
return "<div class=\\"steprow"+(fx.probe?" fail":"")+"\\" data-i=\\""+i+"\\"><span class=\\"st\\">\\u25cb</span><span class=\\"nm\\">"+(fx.probe?"
|
|
1422
|
+
return "<div class=\\"steprow"+(fx.probe?" fail":"")+"\\" data-i=\\""+i+"\\"><span class=\\"st\\">\\u25cb</span><span class=\\"nm\\">"+(fx.probe?"probe \\u00b7 ":"")+fx.label+"</span></div>";
|
|
741
1423
|
}).join("");
|
|
742
1424
|
Array.prototype.slice.call(document.querySelectorAll(".steprow")).forEach(function(row){
|
|
743
1425
|
row.addEventListener("click", function(){ jumpTo(Number(row.getAttribute("data-i"))); });
|
|
@@ -748,7 +1430,7 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
748
1430
|
if (!row) return;
|
|
749
1431
|
Array.prototype.slice.call(document.querySelectorAll(".steprow")).forEach(function(r){ r.classList.remove("active"); });
|
|
750
1432
|
if (verdict==null){ row.classList.add("active"); row.querySelector(".st").textContent="\\u25b8"; return; }
|
|
751
|
-
row.querySelector(".st").textContent = verdict==="PASS"?"\\
|
|
1433
|
+
row.querySelector(".st").textContent = verdict==="PASS"?"\\u2713":verdict==="FAIL"?"\\u2715":"\\u2298";
|
|
752
1434
|
}
|
|
753
1435
|
|
|
754
1436
|
var caption = document.getElementById("caption");
|
|
@@ -780,7 +1462,7 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
780
1462
|
var pass = results.filter(function(r){ return r.verdict==="PASS"; });
|
|
781
1463
|
var verdict = fails.length ? "FAIL" : (pass.length ? "PASS" : "BLOCKED");
|
|
782
1464
|
var html = "<span class=\\"badge "+verdict.toLowerCase()+"\\">"+verdict+"</span>";
|
|
783
|
-
html += "<span class=\\"sub\\" style=\\"color:var(--fg-50)\\">\\
|
|
1465
|
+
html += "<span class=\\"sub\\" style=\\"color:var(--fg-50)\\">\\u2713 "+pass.length+" \\u00b7 \\u2715 "+fails.length+" \\u00b7 \\u2298 "+blocked.length+"</span>";
|
|
784
1466
|
if (fails.length) html += "<span class=\\"issues\\">"+fails.map(function(f){ return f.unitId+": "+(f.checks[0]?f.checks[0].label:""); }).join(" \\u00b7 ")+"</span>";
|
|
785
1467
|
else if (!pass.length && blocked.length) html += "<span class=\\"issues blk\\">"+blocked[0].blockedReason+"</span>";
|
|
786
1468
|
fs.innerHTML = html;
|
|
@@ -793,7 +1475,7 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
793
1475
|
var d = frameDoc(); if (!d || staged) return;
|
|
794
1476
|
var st = d.createElement("style");
|
|
795
1477
|
st.textContent =
|
|
796
|
-
".header,.custom-view-banner,.failover-banner,.indexing-banner,.budget-banner
|
|
1478
|
+
".header,.custom-view-banner,.failover-banner,.indexing-banner,.budget-banner,#task-detail-view .input-area{display:none!important}" +
|
|
797
1479
|
"body{overflow:auto!important;height:auto!important}" +
|
|
798
1480
|
".content{padding:0!important}#task-detail-view{padding:18px 22px!important}";
|
|
799
1481
|
d.head.appendChild(st);
|
|
@@ -812,7 +1494,7 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
812
1494
|
var row = document.getElementById("cl-"+invId);
|
|
813
1495
|
if (!row) return;
|
|
814
1496
|
row.className = "cl-row run " + verdict.toLowerCase();
|
|
815
|
-
row.querySelector(".cl-st").textContent = verdict==="PASS"?"\\
|
|
1497
|
+
row.querySelector(".cl-st").textContent = verdict==="PASS"?"\\u2713":verdict==="FAIL"?"\\u2715":"\\u2298";
|
|
816
1498
|
if (text) row.querySelector(".cl-tx").textContent = text;
|
|
817
1499
|
}
|
|
818
1500
|
|
|
@@ -823,6 +1505,8 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
823
1505
|
if (invId==="H3") return d.querySelector('[data-verify-unit="outcome"]');
|
|
824
1506
|
if (invId==="H5") return d.querySelector('[data-verify-unit="filetree"]') || d.querySelector('[data-verify-unit="outcome"]');
|
|
825
1507
|
if (invId==="H6") return d.querySelector('.recovery-card,.crash-recovery,.hook-block-card') || d.querySelector('[data-verify-unit="detail"]');
|
|
1508
|
+
if (invId==="H13") return d.querySelector('[data-verify-unit="home-hero"]') || d.querySelector('[data-verify-unit="detail"]');
|
|
1509
|
+
if (invId==="H14") return d.querySelector('[data-verify-unit="decisions"]') || d.querySelector('[data-verify-unit="home-hero"]') || d.querySelector('[data-verify-unit="detail"]');
|
|
826
1510
|
return d.querySelector('[data-verify-unit="detail"]');
|
|
827
1511
|
}
|
|
828
1512
|
// A granular, human-readable line for what THIS check observed.
|
|
@@ -833,6 +1517,8 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
833
1517
|
if (inv.id==="H3") return "H3 \\u00b7 outcome renders "+s.outcomeItems+" item(s)";
|
|
834
1518
|
if (inv.id==="H5") return "H5 \\u00b7 claims "+s.filesClaimed+" files, renders "+s.filetreeRendered+" row(s)";
|
|
835
1519
|
if (inv.id==="H6") return "H6 \\u00b7 status='"+s.status+"', reason shown="+s.hasExplanation;
|
|
1520
|
+
if (inv.id==="H13" && s.hero && s.hero.expected) return "H13 \\u00b7 hero claims a="+s.hero.agents+" v="+s.hero.verified+" c="+s.hero.caught+" r="+s.hero.running+" \\u00b7 derived a="+s.hero.expected.agents+" v="+s.hero.expected.verified+" c="+s.hero.expected.caught+" r="+s.hero.expected.running;
|
|
1521
|
+
if (inv.id==="H14" && s.decisions) return "H14 \\u00b7 queue "+(s.decisions.present?("claims N="+s.decisions.n+", renders "+s.decisions.rows+" row(s)"):"absent")+" \\u00b7 derived "+s.decisions.expected+" decision(s)";
|
|
836
1522
|
return inv.desc;
|
|
837
1523
|
}
|
|
838
1524
|
|
|
@@ -845,19 +1531,19 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
845
1531
|
var d = frameDoc();
|
|
846
1532
|
if (r.verdict==="BLOCKED"){
|
|
847
1533
|
// brief, dim — honest "nothing to observe", no theatrics
|
|
848
|
-
clSet(inv.id, "BLOCKED", inv.title+" \\
|
|
849
|
-
showCaption("\\
|
|
1534
|
+
clSet(inv.id, "BLOCKED", inv.title+" \\u00b7 n/a");
|
|
1535
|
+
showCaption("\\u2298 " + narrate(inv, s, r));
|
|
850
1536
|
return sleep(Math.round(KEY_HOLD*0.45)).then(function(){ showCaption(null); return r; });
|
|
851
1537
|
}
|
|
852
1538
|
var el = targetFor(inv.id, d);
|
|
853
1539
|
if (el && el.scrollIntoView) try { el.scrollIntoView({block:"center"}); } catch(e){}
|
|
854
1540
|
return sleep(120).then(function(){
|
|
855
1541
|
drawRing(el);
|
|
856
|
-
showCaption("\\
|
|
1542
|
+
showCaption("\\u25CF " + narrate(inv, s, r));
|
|
857
1543
|
return sleep(KEY_HOLD);
|
|
858
1544
|
}).then(function(){
|
|
859
1545
|
removeRing();
|
|
860
|
-
var verdictWord = r.verdict==="FAIL" ? " \\
|
|
1546
|
+
var verdictWord = r.verdict==="FAIL" ? " \\u203A \\u2715 FAIL" : " \\u203A \\u2713 PASS";
|
|
861
1547
|
showCaption((r.verdict==="FAIL"?"\\u26a0 ":"") + narrate(inv, s, r) + verdictWord);
|
|
862
1548
|
clSet(inv.id, r.verdict, inv.title);
|
|
863
1549
|
return sleep(Math.round(KEY_HOLD*0.7));
|
|
@@ -871,7 +1557,7 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
871
1557
|
idx = i;
|
|
872
1558
|
markStep(i, null);
|
|
873
1559
|
clReset();
|
|
874
|
-
document.getElementById("stage-title").innerHTML = "case " + (i+1) + "/" + FIXTURES.length + " · <strong>" + (fx.probe?"
|
|
1560
|
+
document.getElementById("stage-title").innerHTML = "case " + (i+1) + "/" + FIXTURES.length + " · <strong>" + (fx.probe?"probe \\u00b7 ":"") + fx.label + "</strong> <span class=\\"dim\\">/ "+fx.id+"</span>";
|
|
875
1561
|
document.getElementById("framestatus").innerHTML = "<span class=\\"sub\\" style=\\"color:var(--fg-40)\\">opening case\\u2026</span>";
|
|
876
1562
|
setStageVerdict(null);
|
|
877
1563
|
document.getElementById("prog").style.width = ((i)/FIXTURES.length*100)+"%";
|
|
@@ -881,8 +1567,15 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
881
1567
|
return sleep(120);
|
|
882
1568
|
}).then(function(){
|
|
883
1569
|
stageClean();
|
|
884
|
-
showCaption("
|
|
885
|
-
|
|
1570
|
+
showCaption("open case \\u203A showD('"+fx.label+"')");
|
|
1571
|
+
// S20 · lens fixtures replay through the page's own applyLens — the
|
|
1572
|
+
// human watches the recomposed surface, not the default one.
|
|
1573
|
+
try {
|
|
1574
|
+
var w = frameWin();
|
|
1575
|
+
if (fx.lens && typeof w.applyLens === "function") w.applyLens(fx.lens);
|
|
1576
|
+
else pinLensDefault();
|
|
1577
|
+
w.showD(fx.sid || fx.id);
|
|
1578
|
+
} catch(e){}
|
|
886
1579
|
return sleep(Math.round(KEY_HOLD*0.7));
|
|
887
1580
|
}).then(function(){
|
|
888
1581
|
showCaption(null);
|
|
@@ -905,15 +1598,16 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
905
1598
|
|
|
906
1599
|
function jumpTo(i){ playing=false; openReplay(); playFixture(i); }
|
|
907
1600
|
function advance(){
|
|
908
|
-
if (idx+1 >= FIXTURES.length){ playing=false; document.getElementById("play").innerHTML="&#
|
|
1601
|
+
if (idx+1 >= FIXTURES.length){ playing=false; document.getElementById("play").innerHTML="▶ Replay again"; finishReplay(); return; }
|
|
909
1602
|
playFixture(idx+1).then(function(){ if(playing) setTimeout(advance, DWELL); });
|
|
910
1603
|
}
|
|
911
1604
|
function startReplay(){
|
|
912
1605
|
replayResults = []; idx = 0; playing = true;
|
|
913
|
-
document.getElementById("play").innerHTML = "&#
|
|
1606
|
+
document.getElementById("play").innerHTML = "▮▮ Pause";
|
|
914
1607
|
playFixture(0).then(function(){ if(playing) setTimeout(advance, DWELL); });
|
|
915
1608
|
}
|
|
916
1609
|
function finishReplay(){
|
|
1610
|
+
restoreLens();
|
|
917
1611
|
var c = classify(replayResults);
|
|
918
1612
|
window.__verify_replay = { done:true, results:replayResults, summary:c };
|
|
919
1613
|
}
|
|
@@ -929,11 +1623,11 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
929
1623
|
var recStream=null, recorder=null, recChunks=[], recClips=[];
|
|
930
1624
|
document.getElementById("record").addEventListener("click", function(){
|
|
931
1625
|
var btn = this;
|
|
932
|
-
if (recStream){ stopRecording(); btn.innerHTML="&#
|
|
1626
|
+
if (recStream){ stopRecording(); btn.innerHTML="● Record clips"; return; }
|
|
933
1627
|
navigator.mediaDevices.getDisplayMedia({ video:{frameRate:30}, audio:false, preferCurrentTab:true })
|
|
934
1628
|
.then(function(stream){
|
|
935
|
-
recStream = stream; btn.innerHTML="&#
|
|
936
|
-
stream.getVideoTracks()[0].addEventListener("ended", function(){ stopRecording(); btn.innerHTML="&#
|
|
1629
|
+
recStream = stream; btn.innerHTML="● Recording \\u00b7 stop";
|
|
1630
|
+
stream.getVideoTracks()[0].addEventListener("ended", function(){ stopRecording(); btn.innerHTML="● Record clips"; });
|
|
937
1631
|
// record one continuous clip across the whole replay (simple + reliable);
|
|
938
1632
|
// download offered on stop.
|
|
939
1633
|
recChunks = [];
|
|
@@ -947,7 +1641,7 @@ export const VERIFY_HTML = `<!doctype html>
|
|
|
947
1641
|
recorder.start(200);
|
|
948
1642
|
startReplay();
|
|
949
1643
|
})
|
|
950
|
-
.catch(function(e){ btn.innerHTML="&#
|
|
1644
|
+
.catch(function(e){ btn.innerHTML="● Record clips"; alert("recording cancelled: "+e); });
|
|
951
1645
|
});
|
|
952
1646
|
function stopRecording(){
|
|
953
1647
|
try { if(recorder && recorder.state!=="inactive") recorder.stop(); } catch(e){}
|