replay-labs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +134 -0
- package/examples/password-reset-transcript.md +27 -0
- package/examples/password-reset.diff +101 -0
- package/package.json +47 -0
- package/scripts/capture-git-working-diff.js +56 -0
- package/scripts/create-added-files-diff.js +33 -0
- package/scripts/extract-claude-transcript.js +86 -0
- package/scripts/extract-codex-transcript.js +119 -0
- package/src/cli.js +316 -0
- package/src/discovery.js +715 -0
- package/src/generate.js +406 -0
- package/src/ingest.js +124 -0
- package/src/interaction.js +1161 -0
- package/src/lab-ui.js +1339 -0
- package/src/modules.js +643 -0
- package/src/overview.js +147 -0
- package/src/patterns.js +322 -0
- package/src/pipeline.js +68 -0
- package/src/report.js +516 -0
- package/src/review.js +238 -0
- package/src/server.js +199 -0
- package/src/storage.js +34 -0
package/src/lab-ui.js
ADDED
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
// Replay Labs UI v3.
|
|
2
|
+
// Dark IDE-grade workspace, editorial typography, one accent, code chrome,
|
|
3
|
+
// terminal moments with staggered reveals, and reuse notes worth keeping.
|
|
4
|
+
|
|
5
|
+
export function generateLabHtml({ goal, module: baseModule, evidence, patternHref, homeHref }) {
|
|
6
|
+
const module = patternHref ? { ...baseModule, patternHref } : baseModule;
|
|
7
|
+
const evidenceHtml = renderDiff(evidence);
|
|
8
|
+
const storageKey = `replay-lab-v4:${hash(goal + module.name)}`;
|
|
9
|
+
const payload = JSON.stringify({ module, evidenceHtml, evidenceRaw: evidence, storageKey, goal })
|
|
10
|
+
.replaceAll("<", "\\u003c");
|
|
11
|
+
const wordmark = homeHref
|
|
12
|
+
? `<a class="wordmark" href="${escapeHtml(homeHref)}" style="color:inherit">replay labs<em>.</em><span>Mission Lab</span></a>`
|
|
13
|
+
: `<div class="wordmark">replay labs<em>.</em><span>Mission Lab</span></div>`;
|
|
14
|
+
|
|
15
|
+
return `<!doctype html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="utf-8" />
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
20
|
+
<title>${escapeHtml(module.name)} — Replay Labs</title>
|
|
21
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
22
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
23
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
24
|
+
<style>
|
|
25
|
+
:root {
|
|
26
|
+
--bg: #0c0e11; --panel: #14171c; --panel2: #191d23;
|
|
27
|
+
--line: #242a32; --line2: #303845;
|
|
28
|
+
--ink: #e9e7e2; --muted: #98a1ac; --faint: #67707c;
|
|
29
|
+
--accent: #34d399; --accent-dim: #34d39922; --accent-ink: #052e1e;
|
|
30
|
+
--fail: #f87171; --fail-dim: #f8717118; --warn: #fbbf24;
|
|
31
|
+
--code-bg: #0f1216;
|
|
32
|
+
--sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
33
|
+
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
34
|
+
}
|
|
35
|
+
* { box-sizing: border-box; }
|
|
36
|
+
html { scroll-behavior: smooth; }
|
|
37
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: var(--sans);
|
|
38
|
+
font-size: 15.5px; line-height: 1.6; -webkit-font-smoothing: antialiased; }
|
|
39
|
+
::selection { background: var(--accent); color: var(--accent-ink); }
|
|
40
|
+
a { color: var(--accent); text-decoration: none; }
|
|
41
|
+
a:hover { text-decoration: underline; }
|
|
42
|
+
button { font: inherit; cursor: pointer; }
|
|
43
|
+
|
|
44
|
+
header.top {
|
|
45
|
+
position: sticky; top: 0; z-index: 10;
|
|
46
|
+
display: flex; align-items: center; gap: 28px;
|
|
47
|
+
padding: 14px 28px; background: #0c0e11e6; backdrop-filter: blur(8px);
|
|
48
|
+
border-bottom: 1px solid var(--line);
|
|
49
|
+
}
|
|
50
|
+
.wordmark { font-family: var(--mono); font-weight: 700; font-size: 15px; letter-spacing: .04em; }
|
|
51
|
+
.wordmark em { color: var(--accent); font-style: normal; }
|
|
52
|
+
.wordmark span { color: var(--faint); font-weight: 400; margin-left: 10px; font-size: 12px;
|
|
53
|
+
letter-spacing: .12em; text-transform: uppercase; }
|
|
54
|
+
.stepper { display: flex; align-items: center; gap: 0; margin-left: auto; }
|
|
55
|
+
.step { display: flex; align-items: center; gap: 8px; background: none; border: 0; color: var(--faint);
|
|
56
|
+
padding: 4px 8px; font-size: 12.5px; font-weight: 600; letter-spacing: .02em; }
|
|
57
|
+
.step .pip { width: 24px; height: 24px; border-radius: 50%; display: grid; place-items: center;
|
|
58
|
+
border: 1.5px solid var(--line2); font-family: var(--mono); font-size: 11px; color: var(--muted);
|
|
59
|
+
transition: all .2s ease; }
|
|
60
|
+
.step.active { color: var(--ink); }
|
|
61
|
+
.step.active .pip { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 4px var(--accent-dim); }
|
|
62
|
+
.step.done { color: var(--muted); }
|
|
63
|
+
.step.done .pip { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 700; }
|
|
64
|
+
.step-link { width: 22px; height: 1px; background: var(--line2); }
|
|
65
|
+
.ghost { background: none; border: 1px solid var(--line); color: var(--muted); border-radius: 8px;
|
|
66
|
+
padding: 7px 12px; font-size: 12.5px; font-weight: 600; }
|
|
67
|
+
.ghost:hover { border-color: var(--line2); color: var(--ink); }
|
|
68
|
+
|
|
69
|
+
main { max-width: 880px; margin: 0 auto; padding: 44px 24px 90px; }
|
|
70
|
+
.hero { margin-bottom: 34px; }
|
|
71
|
+
.eyebrow { font-family: var(--mono); font-size: 11px; font-weight: 500; letter-spacing: .16em;
|
|
72
|
+
text-transform: uppercase; color: var(--faint); margin-bottom: 14px; }
|
|
73
|
+
.eyebrow b { color: var(--accent); font-weight: 700; }
|
|
74
|
+
h1 { font-size: 38px; font-weight: 800; letter-spacing: -0.02em; margin: 0 0 10px; line-height: 1.1; }
|
|
75
|
+
.tagline { color: var(--muted); font-size: 17px; margin: 0 0 20px; max-width: 640px; }
|
|
76
|
+
.chips { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; }
|
|
77
|
+
.chip { display: inline-flex; align-items: baseline; gap: 8px; border: 1px solid var(--line);
|
|
78
|
+
background: var(--panel); border-radius: 999px; padding: 7px 14px; font-size: 13px; }
|
|
79
|
+
.chip i { font-style: normal; font-family: var(--mono); font-size: 10px; letter-spacing: .14em;
|
|
80
|
+
text-transform: uppercase; color: var(--faint); }
|
|
81
|
+
.chip b { font-weight: 600; }
|
|
82
|
+
.chip.smell b { color: var(--fail); }
|
|
83
|
+
.chip.pattern b { color: var(--accent); }
|
|
84
|
+
.goal-line { color: var(--faint); font-size: 13.5px; }
|
|
85
|
+
.goal-line a { font-weight: 600; }
|
|
86
|
+
|
|
87
|
+
.stage-card { background: var(--panel); border: 1px solid var(--line); border-radius: 16px;
|
|
88
|
+
padding: 30px 32px; animation: fadeUp .35s ease both; }
|
|
89
|
+
.beat { background: var(--panel); border: 1px solid var(--line); border-radius: 16px;
|
|
90
|
+
padding: 26px 30px; margin-bottom: 16px; animation: fadeUp .4s ease both; }
|
|
91
|
+
.beat.done { border-color: #34d39930; }
|
|
92
|
+
.beat-eyebrow { font-family: var(--mono); font-size: 11px; letter-spacing: .16em;
|
|
93
|
+
text-transform: uppercase; color: var(--accent); margin-bottom: 8px; font-weight: 700; }
|
|
94
|
+
.beat.done .beat-eyebrow { color: var(--muted); }
|
|
95
|
+
.bprompt { font-size: 17px; font-weight: 600; margin: 0 0 14px; line-height: 1.4; }
|
|
96
|
+
.crashbox { background: var(--code-bg); border: 1px solid #f8717133; border-radius: 12px;
|
|
97
|
+
padding: 14px 16px; margin: 0 0 14px; font-family: var(--mono); font-size: 12.5px;
|
|
98
|
+
line-height: 1.6; color: #aeb7c2; overflow-x: auto; }
|
|
99
|
+
.crashbox .t-line { white-space: pre; }
|
|
100
|
+
.crashbox .err { color: var(--fail); font-weight: 700; }
|
|
101
|
+
.thread-intro { font-size: 15px; font-weight: 600; margin: 18px 0 10px; }
|
|
102
|
+
.rc { display: flex; gap: 12px; align-items: flex-start; width: 100%; text-align: left;
|
|
103
|
+
background: var(--panel2); border: 1px solid var(--line); border-radius: 12px;
|
|
104
|
+
padding: 14px 16px; margin-bottom: 10px; color: var(--ink); cursor: pointer;
|
|
105
|
+
transition: border-color .15s, transform .15s; }
|
|
106
|
+
.rc:hover { border-color: var(--line2); transform: translateY(-1px); }
|
|
107
|
+
.rc .avatar { flex: 0 0 30px; height: 30px; border-radius: 50%; display: grid; place-items: center;
|
|
108
|
+
background: var(--bg); border: 1px solid var(--line2); font-family: var(--mono); font-size: 11px;
|
|
109
|
+
font-weight: 700; color: var(--muted); }
|
|
110
|
+
.rc .handle { font-family: var(--mono); font-size: 12px; color: var(--faint); margin-bottom: 4px; }
|
|
111
|
+
.rc p { margin: 0; font-size: 14px; line-height: 1.55; }
|
|
112
|
+
.rc .approve { margin-left: auto; align-self: center; font-family: var(--mono); font-size: 11px;
|
|
113
|
+
color: var(--faint); white-space: nowrap; opacity: 0; transition: opacity .15s; }
|
|
114
|
+
.rc:hover .approve { opacity: 1; color: var(--accent); }
|
|
115
|
+
.rc.is-right { border-color: var(--accent); background: var(--accent-dim); cursor: default; }
|
|
116
|
+
.rc.is-right .avatar { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
|
|
117
|
+
.rc.is-wrong { border-color: var(--fail); background: var(--fail-dim); animation: shake .3s ease; }
|
|
118
|
+
.rc.is-faded { opacity: .45; cursor: default; }
|
|
119
|
+
.verdict-note { border-left: 3px solid var(--line2); padding: 4px 0 4px 14px; margin: 4px 0 14px;
|
|
120
|
+
font-size: 13.5px; color: var(--muted); line-height: 1.55; }
|
|
121
|
+
.verdict-note.good { border-color: var(--accent); }
|
|
122
|
+
.verdict-note.bad { border-color: var(--fail); }
|
|
123
|
+
.btn-lint { background: none; border: 1px solid var(--line2); color: var(--muted); border-radius: 10px;
|
|
124
|
+
padding: 12px 18px; font-family: var(--mono); font-size: 13.5px; font-weight: 600; cursor: pointer;
|
|
125
|
+
transition: border-color .15s, color .15s; }
|
|
126
|
+
.btn-lint:hover { color: var(--ink); border-color: var(--warn); }
|
|
127
|
+
.term.lint { border-color: #fbbf2433; }
|
|
128
|
+
.dot-cov { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--line2);
|
|
129
|
+
margin-left: 8px; vertical-align: 1px; transition: background .25s; }
|
|
130
|
+
.dot-cov.on { background: var(--accent); }
|
|
131
|
+
.mode-row { display: flex; gap: 8px; margin: 0 0 14px; }
|
|
132
|
+
.mode-btn { background: none; border: 1px solid var(--line); color: var(--faint); border-radius: 8px;
|
|
133
|
+
padding: 7px 14px; font-size: 12.5px; font-weight: 600; cursor: pointer; }
|
|
134
|
+
.mode-btn.on { border-color: var(--accent); color: var(--ink); background: var(--accent-dim); }
|
|
135
|
+
.bin-label { font-family: var(--mono); font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
|
|
136
|
+
color: var(--faint); margin: 14px 0 8px; }
|
|
137
|
+
.blk { display: block; width: 100%; text-align: left; font-family: var(--mono); font-size: 12.5px;
|
|
138
|
+
line-height: 1.55; background: var(--code-bg); border: 1px solid var(--line); border-radius: 10px;
|
|
139
|
+
padding: 10px 14px; margin-bottom: 8px; color: #d6dde6; cursor: pointer; white-space: pre-wrap;
|
|
140
|
+
transition: border-color .15s, transform .15s; }
|
|
141
|
+
.blk:hover { border-color: var(--accent); transform: translateY(-1px); }
|
|
142
|
+
.asm-line { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start;
|
|
143
|
+
font-family: var(--mono); font-size: 12.5px; line-height: 1.55; white-space: pre-wrap;
|
|
144
|
+
border-bottom: 1px dashed var(--line); padding: 8px 4px; cursor: pointer; color: #d6dde6; }
|
|
145
|
+
.asm-line:hover { background: #f8717111; }
|
|
146
|
+
.asm-line .rm { color: var(--faint); font-size: 11px; flex: 0 0 auto; }
|
|
147
|
+
.asm-line:hover .rm { color: var(--fail); }
|
|
148
|
+
.asm-empty { color: var(--faint); font-size: 13.5px; padding: 18px 14px; }
|
|
149
|
+
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
|
|
150
|
+
.pchip { font-size: 12px; border: 1px solid var(--line); background: var(--panel); border-radius: 999px;
|
|
151
|
+
padding: 4px 11px; color: var(--muted); cursor: pointer; transition: border-color .15s, color .15s; }
|
|
152
|
+
.pchip:hover { border-color: var(--accent); color: var(--ink); }
|
|
153
|
+
.stage-eyebrow { font-family: var(--mono); font-size: 11px; letter-spacing: .16em;
|
|
154
|
+
text-transform: uppercase; color: var(--accent); margin-bottom: 10px; font-weight: 700; }
|
|
155
|
+
.stage-card h2 { font-size: 24px; font-weight: 700; letter-spacing: -0.01em; margin: 0 0 6px; line-height: 1.25; }
|
|
156
|
+
.stage-copy { color: var(--muted); margin: 0 0 22px; max-width: 620px; }
|
|
157
|
+
.mission-strip { display: flex; align-items: center; justify-content: space-between; gap: 14px;
|
|
158
|
+
border: 1px solid var(--line); border-radius: 12px; background: var(--panel); padding: 14px 16px;
|
|
159
|
+
margin: 0 0 16px; }
|
|
160
|
+
.mission-strip b { display: block; font-size: 14px; margin-bottom: 2px; }
|
|
161
|
+
.mission-strip span { color: var(--muted); font-size: 13px; }
|
|
162
|
+
.mission-toggle { flex: 0 0 auto; border: 1px solid var(--line2); background: transparent; color: var(--muted);
|
|
163
|
+
border-radius: 999px; padding: 8px 12px; font-family: var(--mono); font-size: 11px;
|
|
164
|
+
letter-spacing: .1em; text-transform: uppercase; font-weight: 700; }
|
|
165
|
+
.mission-toggle.on { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
|
|
166
|
+
.mission-brief { border: 1px solid #34d39933; border-radius: 14px; background: #34d3990d;
|
|
167
|
+
margin: 0 0 16px; padding: 18px; }
|
|
168
|
+
.mission-brief .eyebrow { margin-bottom: 8px; }
|
|
169
|
+
.mission-brief h2 { font-size: 22px; line-height: 1.2; margin: 0 0 8px; letter-spacing: -0.01em; }
|
|
170
|
+
.mission-brief p { color: #cdd4dc; margin: 0 0 14px; max-width: 720px; }
|
|
171
|
+
.mission-brief ul { margin: 0; padding-left: 18px; color: var(--muted); }
|
|
172
|
+
.mission-brief li { margin: 4px 0; }
|
|
173
|
+
.mission-starter { border: 1px solid #34d39933; background: #34d3990d; border-radius: 12px;
|
|
174
|
+
padding: 14px; margin: 0 0 14px; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
175
|
+
.mission-starter b { display: block; margin-bottom: 4px; }
|
|
176
|
+
.mission-starter p { margin: 0; color: var(--muted); font-size: 14px; }
|
|
177
|
+
.mission-starter .btn-lint { white-space: nowrap; }
|
|
178
|
+
|
|
179
|
+
details.evidence { border: 1px solid var(--line); border-radius: 12px; background: var(--panel2);
|
|
180
|
+
margin: 0 0 22px; overflow: hidden; }
|
|
181
|
+
details.evidence summary { cursor: pointer; list-style: none; padding: 13px 16px; display: flex;
|
|
182
|
+
align-items: center; gap: 10px; font-weight: 600; font-size: 13.5px; color: var(--muted); }
|
|
183
|
+
details.evidence summary::before { content: "▸"; color: var(--accent); transition: transform .15s; }
|
|
184
|
+
details.evidence[open] summary::before { transform: rotate(90deg); }
|
|
185
|
+
details.evidence summary:hover { color: var(--ink); }
|
|
186
|
+
.evidence-body { padding: 0 16px 16px; }
|
|
187
|
+
.lens { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
|
|
188
|
+
.lens b { width: 100%; font-size: 12.5px; color: var(--muted); font-weight: 600; }
|
|
189
|
+
.lens span { border: 1px solid var(--line); border-radius: 999px; padding: 4px 11px;
|
|
190
|
+
font-size: 12.5px; color: var(--muted); background: var(--panel); }
|
|
191
|
+
|
|
192
|
+
.code-win { border: 1px solid var(--line); border-radius: 12px; background: var(--code-bg);
|
|
193
|
+
overflow: hidden; margin: 0 0 18px; }
|
|
194
|
+
.code-bar { display: flex; align-items: center; gap: 8px; padding: 10px 14px;
|
|
195
|
+
border-bottom: 1px solid var(--line); }
|
|
196
|
+
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--line2); }
|
|
197
|
+
.dot.r { background: #f8717155; } .dot.y { background: #fbbf2455; } .dot.g { background: #34d39955; }
|
|
198
|
+
.code-bar .fname { margin-left: 8px; font-family: var(--mono); font-size: 12px; color: var(--faint); }
|
|
199
|
+
.code-bar .flag { margin-left: auto; font-family: var(--mono); font-size: 10.5px; letter-spacing: .1em;
|
|
200
|
+
text-transform: uppercase; color: var(--fail); border: 1px solid var(--fail-dim);
|
|
201
|
+
background: var(--fail-dim); border-radius: 5px; padding: 2px 8px; }
|
|
202
|
+
.code-tools { margin-left: auto; display: flex; align-items: center; gap: 6px; }
|
|
203
|
+
.code-tools .flag { margin-left: 6px; }
|
|
204
|
+
.view-toggle { border: 1px solid var(--line); background: transparent; color: var(--faint);
|
|
205
|
+
border-radius: 7px; padding: 4px 8px; font-family: var(--mono); font-size: 10.5px;
|
|
206
|
+
letter-spacing: .06em; text-transform: uppercase; }
|
|
207
|
+
.view-toggle.on { color: var(--accent-ink); background: var(--accent); border-color: var(--accent); font-weight: 700; }
|
|
208
|
+
.code-body { display: flex; }
|
|
209
|
+
.gutter { padding: 14px 0 14px 16px; text-align: right; user-select: none; color: #3d4654;
|
|
210
|
+
font-family: var(--mono); font-size: 13px; line-height: 1.62; white-space: pre; }
|
|
211
|
+
pre.code { margin: 0; padding: 14px 18px; flex: 1; overflow: auto; font-family: var(--mono);
|
|
212
|
+
font-size: 13px; line-height: 1.62; color: #d6dde6; max-height: 440px; }
|
|
213
|
+
.k { color: #b794f6; } .s { color: #7ee0a3; } .m { color: #5b6472; font-style: italic; }
|
|
214
|
+
code.inl { font-family: var(--mono); font-size: .88em; background: #ffffff0d;
|
|
215
|
+
border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px; color: #cdb9f9; }
|
|
216
|
+
.cl { display: block; cursor: pointer; border-radius: 4px; padding: 0 6px; margin: 0 -6px; }
|
|
217
|
+
.cl:hover { background: #ffffff0a; }
|
|
218
|
+
.cl-hit { background: #34d39922; outline: 1px solid #34d39966; }
|
|
219
|
+
.cl-miss { background: #f8717122; outline: 1px solid #f8717144; }
|
|
220
|
+
.code-win.done .cl { cursor: default; }
|
|
221
|
+
.code-win.done .cl:hover { background: transparent; }
|
|
222
|
+
.code-win.done .cl-hit:hover { background: #34d39922; }
|
|
223
|
+
.f-label { display: block; font-size: 13px; font-weight: 600; color: var(--muted); margin: 14px 0 6px; }
|
|
224
|
+
textarea.f-small { min-height: 56px; }
|
|
225
|
+
pre.diff { margin: 0; padding: 14px 16px; border-radius: 10px; background: var(--code-bg);
|
|
226
|
+
border: 1px solid var(--line); overflow: auto; font-family: var(--mono); font-size: 12.5px;
|
|
227
|
+
line-height: 1.6; color: #aeb7c2; max-height: 360px; }
|
|
228
|
+
.d-add { color: #7ee0a3; } .d-del { color: #f88; } .d-hunk { color: #6f9ff0; }
|
|
229
|
+
.d-meta { color: var(--faint); }
|
|
230
|
+
.d-prefix { display: inline-block; min-width: 18px; color: var(--faint); user-select: none; }
|
|
231
|
+
.d-code { white-space: pre-wrap; }
|
|
232
|
+
.diff-readable .d-prefix { display: none; }
|
|
233
|
+
.diff-readable .d-meta, .diff-readable .d-hunk { display: none; }
|
|
234
|
+
.diff-readable .cl, pre.diff.diff-readable .d-add, pre.diff.diff-readable .d-del, pre.diff.diff-readable .d-line {
|
|
235
|
+
display: block;
|
|
236
|
+
border-left: 2px solid transparent; padding: 1px 0 1px 14px; margin: 1px 0;
|
|
237
|
+
background: transparent; border-radius: 0;
|
|
238
|
+
}
|
|
239
|
+
.diff-readable .d-add { border-left-color: var(--accent); }
|
|
240
|
+
.diff-readable .d-del { border-left-color: var(--fail); opacity: .62; }
|
|
241
|
+
.diff-readable .d-meta, .diff-readable .d-hunk { display: none; }
|
|
242
|
+
.diff-readable .d-code { min-width: 0; }
|
|
243
|
+
.d-mark { background: #fbbf2426; color: #fbd34d; border-radius: 3px; padding: 0 2px; }
|
|
244
|
+
|
|
245
|
+
.choices { display: grid; gap: 10px; margin: 18px 0; }
|
|
246
|
+
.choice { display: flex; gap: 14px; align-items: flex-start; text-align: left; width: 100%;
|
|
247
|
+
background: var(--panel2); border: 1px solid var(--line); border-radius: 12px; padding: 15px 16px;
|
|
248
|
+
color: var(--ink); transition: border-color .15s, transform .15s; }
|
|
249
|
+
.choice:hover { border-color: var(--line2); transform: translateY(-1px); }
|
|
250
|
+
.choice .key { flex: 0 0 26px; height: 26px; border-radius: 7px; display: grid; place-items: center;
|
|
251
|
+
background: var(--bg); border: 1px solid var(--line2); font-family: var(--mono); font-size: 12px;
|
|
252
|
+
color: var(--muted); font-weight: 700; }
|
|
253
|
+
.choice b { display: block; font-size: 14.5px; font-weight: 600; margin-bottom: 3px; }
|
|
254
|
+
.choice p { margin: 0; font-size: 13.5px; color: var(--muted); line-height: 1.5; }
|
|
255
|
+
.choice.is-right { border-color: var(--accent); background: var(--accent-dim); }
|
|
256
|
+
.choice.is-right .key { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
|
|
257
|
+
.choice.is-right p { color: var(--ink); }
|
|
258
|
+
.choice.is-wrong { border-color: var(--fail); background: var(--fail-dim); animation: shake .3s ease; }
|
|
259
|
+
.choice.is-wrong .key { border-color: var(--fail); color: var(--fail); }
|
|
260
|
+
.choice.is-faded { opacity: .45; }
|
|
261
|
+
|
|
262
|
+
.pass-line { display: flex; gap: 10px; align-items: baseline; border-top: 1px dashed var(--line);
|
|
263
|
+
padding-top: 14px; margin-top: 4px; font-size: 13px; color: var(--faint); }
|
|
264
|
+
.pass-line b { color: var(--muted); font-weight: 600; white-space: nowrap; }
|
|
265
|
+
|
|
266
|
+
.term { background: var(--code-bg); border: 1px solid var(--line); border-radius: 12px;
|
|
267
|
+
padding: 14px 16px; margin-top: 16px; font-family: var(--mono); font-size: 12.5px; line-height: 1.7; }
|
|
268
|
+
.t-line { animation: fadeUp .3s ease both; }
|
|
269
|
+
.t-cmd { color: var(--muted); } .t-cmd::before { content: "$ "; color: var(--accent); }
|
|
270
|
+
.t-pass { color: var(--accent); } .t-fail { color: var(--fail); } .t-meta { color: var(--faint); }
|
|
271
|
+
.t-sum { color: #c6cdd6; margin-top: 6px; white-space: pre-wrap; }
|
|
272
|
+
.t-mis { color: var(--warn); margin-top: 6px; white-space: pre-wrap; }
|
|
273
|
+
.stamp { display: inline-block; margin-top: 10px; font-weight: 700; letter-spacing: .1em;
|
|
274
|
+
border-radius: 7px; padding: 3px 12px; animation: stampIn .35s cubic-bezier(.2,1.6,.4,1) both; }
|
|
275
|
+
.stamp.pass { color: var(--accent-ink); background: var(--accent); }
|
|
276
|
+
.stamp.fail { color: #2e0c0c; background: var(--fail); }
|
|
277
|
+
.spin::after { content: ""; display: inline-block; width: 10px; height: 10px; margin-left: 8px;
|
|
278
|
+
border: 2px solid var(--faint); border-top-color: var(--accent); border-radius: 50%;
|
|
279
|
+
animation: rot .7s linear infinite; vertical-align: -1px; }
|
|
280
|
+
|
|
281
|
+
.sim-block { border-top: 1px dashed var(--line); margin-top: 22px; padding-top: 18px; animation: fadeUp .35s ease both; }
|
|
282
|
+
.sim-label { font-family: var(--mono); font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
|
|
283
|
+
color: var(--fail); font-weight: 700; margin-bottom: 12px; }
|
|
284
|
+
pre.crash { margin: 0 0 18px; padding: 14px 16px; border-radius: 12px; border: 1px solid #f8717133;
|
|
285
|
+
background: var(--code-bg); font-family: var(--mono); font-size: 12.5px; line-height: 1.6;
|
|
286
|
+
color: #aeb7c2; overflow: auto; }
|
|
287
|
+
.crash .err { color: var(--fail); font-weight: 700; }
|
|
288
|
+
|
|
289
|
+
textarea.editor { display: block; width: 100%; border: 0; outline: none; resize: vertical;
|
|
290
|
+
background: var(--code-bg); color: #d6dde6; font-family: var(--mono); font-size: 13px;
|
|
291
|
+
line-height: 1.62; padding: 14px 18px; min-height: 280px; tab-size: 2; }
|
|
292
|
+
textarea.plan { width: 100%; background: var(--panel2); color: var(--ink); border: 1px solid var(--line);
|
|
293
|
+
border-radius: 12px; padding: 14px 16px; font: 14.5px/1.6 var(--sans); min-height: 150px; resize: vertical; }
|
|
294
|
+
textarea.plan:focus, .code-win:focus-within { border-color: var(--line2); outline: none; }
|
|
295
|
+
textarea.plan::placeholder { color: var(--faint); }
|
|
296
|
+
textarea.plan.f-small { min-height: 56px; }
|
|
297
|
+
|
|
298
|
+
.cta-row { display: flex; align-items: center; gap: 14px; margin-top: 24px; }
|
|
299
|
+
.cta { background: var(--accent); color: var(--accent-ink); font-weight: 700; border: 0;
|
|
300
|
+
border-radius: 10px; padding: 12px 20px; font-size: 14.5px; transition: transform .12s, filter .12s; }
|
|
301
|
+
.cta:hover { filter: brightness(1.08); transform: translateY(-1px); }
|
|
302
|
+
.cta:disabled { background: var(--panel2); color: var(--faint); cursor: not-allowed; transform: none; filter: none; }
|
|
303
|
+
.cta.run { font-family: var(--mono); font-weight: 600; font-size: 13.5px; }
|
|
304
|
+
.cta-note { font-size: 13px; color: var(--faint); }
|
|
305
|
+
.backlink { background: none; border: 0; color: var(--faint); font-size: 13.5px; font-weight: 600; padding: 0; }
|
|
306
|
+
.backlink:hover { color: var(--muted); }
|
|
307
|
+
|
|
308
|
+
.unlock { border: 1px solid var(--accent); border-radius: 12px; background: var(--accent-dim);
|
|
309
|
+
margin-top: 22px; padding: 18px; animation: fadeUp .4s ease both; }
|
|
310
|
+
.unlock h4 { margin: 0 0 12px; font-size: 12px; font-family: var(--mono); letter-spacing: .14em;
|
|
311
|
+
text-transform: uppercase; color: var(--accent); }
|
|
312
|
+
.unlock-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 10px; }
|
|
313
|
+
.ucard { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 13px 15px; }
|
|
314
|
+
.ucard b { display: block; font-size: 13px; margin-bottom: 4px; }
|
|
315
|
+
.ucard p { margin: 0; font-size: 13.5px; color: var(--muted); line-height: 1.5; }
|
|
316
|
+
|
|
317
|
+
.mastery { margin-top: 40px; border: 1px solid var(--accent); border-radius: 18px; padding: 34px;
|
|
318
|
+
background: radial-gradient(120% 140% at 50% 0%, #34d39914 0%, var(--panel) 55%);
|
|
319
|
+
animation: fadeUp .45s ease both; }
|
|
320
|
+
.mastery .eyebrow b { color: var(--accent); }
|
|
321
|
+
.mastery h2 { font-size: 28px; font-weight: 800; letter-spacing: -0.01em; margin: 4px 0 6px; }
|
|
322
|
+
.mastery .sub { color: var(--muted); margin: 0 0 22px; }
|
|
323
|
+
.m-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; }
|
|
324
|
+
.m-card { background: var(--bg); border: 1px solid var(--line); border-radius: 12px; padding: 16px 18px; }
|
|
325
|
+
.m-card i { font-style: normal; display: block; font-family: var(--mono); font-size: 10.5px;
|
|
326
|
+
letter-spacing: .14em; text-transform: uppercase; color: var(--faint); margin-bottom: 7px; }
|
|
327
|
+
.m-card p { margin: 0; font-size: 14px; line-height: 1.55; color: #cdd4dc; }
|
|
328
|
+
.next-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-top: 22px; }
|
|
329
|
+
.next-card { border: 1px dashed var(--line2); border-radius: 12px; padding: 14px 16px; }
|
|
330
|
+
.next-card b { display: block; font-size: 13.5px; margin-bottom: 3px; color: var(--muted); }
|
|
331
|
+
.next-card span { font-size: 12px; color: var(--faint); }
|
|
332
|
+
.next-card .lock { float: right; font-family: var(--mono); font-size: 10px; letter-spacing: .12em;
|
|
333
|
+
color: var(--faint); border: 1px solid var(--line); border-radius: 4px; padding: 1px 7px; }
|
|
334
|
+
|
|
335
|
+
footer { max-width: 880px; margin: 0 auto; padding: 0 24px 50px; color: var(--faint); font-size: 12.5px; }
|
|
336
|
+
|
|
337
|
+
@keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
|
338
|
+
@keyframes shake { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
|
|
339
|
+
@keyframes stampIn { from { opacity: 0; transform: scale(.6) rotate(-4deg); } to { opacity: 1; transform: none; } }
|
|
340
|
+
@keyframes rot { to { transform: rotate(360deg); } }
|
|
341
|
+
@media (max-width: 760px) {
|
|
342
|
+
header.top { flex-wrap: wrap; gap: 12px; padding: 12px 16px; }
|
|
343
|
+
.stepper { margin-left: 0; order: 3; width: 100%; justify-content: space-between; }
|
|
344
|
+
.step-link { display: none; }
|
|
345
|
+
.step span.lbl { display: none; }
|
|
346
|
+
main { padding: 28px 16px 70px; }
|
|
347
|
+
.stage-card { padding: 22px 18px; border-radius: 14px; }
|
|
348
|
+
h1 { font-size: 30px; }
|
|
349
|
+
.mission-strip { display: block; }
|
|
350
|
+
.mission-toggle { margin-top: 10px; }
|
|
351
|
+
}
|
|
352
|
+
</style>
|
|
353
|
+
</head>
|
|
354
|
+
<body>
|
|
355
|
+
<header class="top">
|
|
356
|
+
${wordmark}
|
|
357
|
+
<div class="stepper" id="stepper"></div>
|
|
358
|
+
<button class="ghost" id="reset" type="button">Reset</button>
|
|
359
|
+
</header>
|
|
360
|
+
<main>
|
|
361
|
+
<section class="hero">
|
|
362
|
+
<div class="eyebrow"><b>Practice lab</b> · based on this session evidence · diagnose, break, repair, transfer${module.minutes ? ` · ≈ ${module.minutes} min` : ""}</div>
|
|
363
|
+
<h1>${escapeHtml(module.name)}</h1>
|
|
364
|
+
<p class="tagline">${escapeHtml(module.takeaway)}</p>
|
|
365
|
+
<div class="chips">
|
|
366
|
+
<span class="chip pattern"><i>Pattern</i><b>${escapeHtml(module.challenge.pattern)}</b></span>
|
|
367
|
+
<span class="chip smell"><i>Smell</i><b>${escapeHtml(module.challenge.smell)}</b></span>
|
|
368
|
+
<span class="chip"><i>Check</i><b>${escapeHtml(module.challenge.proof)}</b></span>
|
|
369
|
+
</div>
|
|
370
|
+
<p class="goal-line">Session goal: ${escapeHtml(goal)}${module.patternHref ? ` · <a href="${escapeHtml(module.patternHref)}">catalog entry →</a>` : ""}</p>
|
|
371
|
+
</section>
|
|
372
|
+
<section id="stage-root"></section>
|
|
373
|
+
<section id="mastery-root"></section>
|
|
374
|
+
</main>
|
|
375
|
+
<footer>This lab focuses on one decision from the session so the exercise stays specific.</footer>
|
|
376
|
+
<script>
|
|
377
|
+
const BOOT = ${payload};
|
|
378
|
+
${CLIENT_JS}
|
|
379
|
+
</script>
|
|
380
|
+
</body>
|
|
381
|
+
</html>`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Client runtime. Written without template literals so it can live inside the
|
|
385
|
+
// server-side template safely (no backtick or dollar-brace escaping).
|
|
386
|
+
const CLIENT_JS = String.raw`
|
|
387
|
+
var MODULE = BOOT.module, EVIDENCE_HTML = BOOT.evidenceHtml, EVIDENCE_RAW = BOOT.evidenceRaw || "", KEY = BOOT.storageKey;
|
|
388
|
+
var state = JSON.parse(localStorage.getItem(KEY) || "{}");
|
|
389
|
+
state.complete = state.complete || {}; state.answers = state.answers || {};
|
|
390
|
+
state.choices = state.choices || {}; state.reviews = state.reviews || {};
|
|
391
|
+
if (state.spotPick === undefined) state.spotPick = null;
|
|
392
|
+
if (state.spotFound === undefined) state.spotFound = [];
|
|
393
|
+
if (state.spotHit === undefined) state.spotHit = false;
|
|
394
|
+
if (state.inv === undefined) state.inv = null;
|
|
395
|
+
if (state.lints === undefined) state.lints = {};
|
|
396
|
+
if (state.diffViews === undefined) state.diffViews = {};
|
|
397
|
+
if (state.asm === undefined) state.asm = [];
|
|
398
|
+
if (state.solShown === undefined) state.solShown = false;
|
|
399
|
+
if (state.missionMode === undefined) state.missionMode = false;
|
|
400
|
+
if (state.repairMode === undefined) state.repairMode = MODULE.repairLab && MODULE.repairLab.blocks ? "asm" : "type";
|
|
401
|
+
if (state.arbPick === undefined) state.arbPick = null;
|
|
402
|
+
if (state.arbHit === undefined) state.arbHit = false;
|
|
403
|
+
state.reviewing = null; state.flash = null;
|
|
404
|
+
|
|
405
|
+
var STAGE_META = [
|
|
406
|
+
{ id: "diagnose", label: "Diagnose" },
|
|
407
|
+
{ id: "break", label: "Break" },
|
|
408
|
+
{ id: "repair", label: "Repair" },
|
|
409
|
+
{ id: "transfer", label: "Transfer" }
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
function esc(v) { return String(v == null ? "" : v).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
|
|
413
|
+
function diffToggles(id, extra) {
|
|
414
|
+
var on = state.diffViews[id] === "patch";
|
|
415
|
+
return '<span class="code-tools">' +
|
|
416
|
+
'<button type="button" class="view-toggle' + (on ? " on" : "") + '" aria-pressed="' + (on ? "true" : "false") + '" data-diff-toggle="' + id + '">Patch</button>' +
|
|
417
|
+
(extra || "") +
|
|
418
|
+
"</span>";
|
|
419
|
+
}
|
|
420
|
+
function diffView(id, html) {
|
|
421
|
+
var readable = state.diffViews[id] === "patch" ? "" : " diff-readable";
|
|
422
|
+
return '<div class="code-win"><div class="code-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>' +
|
|
423
|
+
'<span class="fname">session excerpt</span>' + diffToggles(id) +
|
|
424
|
+
'</div><pre class="diff' + readable + '" data-diff-box="' + id + '">' + html + "</pre></div>";
|
|
425
|
+
}
|
|
426
|
+
function diffLineHtml(line) {
|
|
427
|
+
if (line.indexOf("diff --git") === 0 || line.indexOf("index ") === 0 || line.indexOf("---") === 0 || line.indexOf("+++") === 0) {
|
|
428
|
+
return '<span class="d-prefix"></span><span class="d-code">' + esc(line) + "</span>";
|
|
429
|
+
}
|
|
430
|
+
if (line.charAt(0) === "+" || line.charAt(0) === "-") {
|
|
431
|
+
return '<span class="d-prefix">' + esc(line.slice(0, 1)) + '</span><span class="d-code">' + (esc(line.slice(1)) || " ") + "</span>";
|
|
432
|
+
}
|
|
433
|
+
return '<span class="d-prefix"></span><span class="d-code">' + (esc(line) || " ") + "</span>";
|
|
434
|
+
}
|
|
435
|
+
var BT = String.fromCharCode(96);
|
|
436
|
+
var BT_RE = new RegExp(BT + "([^" + BT + "]+)" + BT, "g");
|
|
437
|
+
function fmt(v) { return esc(v).replace(BT_RE, '<code class="inl">$1</code>'); }
|
|
438
|
+
function save() { localStorage.setItem(KEY, JSON.stringify(state)); }
|
|
439
|
+
|
|
440
|
+
function hl(code) {
|
|
441
|
+
var toks = [];
|
|
442
|
+
var s = esc(code).replace(/(\/\/[^\n]*)|('[^'\n]*')|("[^"\n]*")/g, function (m) {
|
|
443
|
+
toks.push(m); return " " + (toks.length - 1) + " ";
|
|
444
|
+
});
|
|
445
|
+
s = s.replace(/\b(export|default|function|return|const|let|var|new|import|from|type|async|await|if|else|try|catch)\b/g, '<span class="k">$1</span>');
|
|
446
|
+
s = s.replace(/ (\d+) /g, function (_, i) {
|
|
447
|
+
var t = toks[Number(i)];
|
|
448
|
+
var cls = t.charAt(0) === "/" ? "m" : "s";
|
|
449
|
+
return '<span class="' + cls + '">' + t + "</span>";
|
|
450
|
+
});
|
|
451
|
+
return s;
|
|
452
|
+
}
|
|
453
|
+
function gutterFor(code) { return code.split("\n").map(function (_, i) { return i + 1; }).join("\n"); }
|
|
454
|
+
|
|
455
|
+
// ---- beats ----
|
|
456
|
+
function missionEnabled() {
|
|
457
|
+
return state.missionMode && /cli contract/i.test(MODULE.name || "");
|
|
458
|
+
}
|
|
459
|
+
function missionMeta(id, fallbackTitle, fallbackCopy) {
|
|
460
|
+
if (!missionEnabled()) return { title: fallbackTitle, copy: fallbackCopy };
|
|
461
|
+
var map = {
|
|
462
|
+
spot: {
|
|
463
|
+
title: "Clear the scraper contract",
|
|
464
|
+
copy: "Before the archive run starts, find the evidence that defines how this command accepts input and how it must fail."
|
|
465
|
+
},
|
|
466
|
+
diagnose: {
|
|
467
|
+
title: "Clear the scraper contract",
|
|
468
|
+
copy: "Decide what responsibility this command needs to carry before it can run against a large archive."
|
|
469
|
+
},
|
|
470
|
+
inv: {
|
|
471
|
+
title: "Expose the bad run",
|
|
472
|
+
copy: "Now remove that contract mentally. Look for the first place a missing or wrong path turns into a confusing run."
|
|
473
|
+
},
|
|
474
|
+
break: {
|
|
475
|
+
title: "Expose the bad run",
|
|
476
|
+
copy: "Choose the failure that would waste time, hide the real problem, or let automation believe the run succeeded."
|
|
477
|
+
},
|
|
478
|
+
crash: {
|
|
479
|
+
title: "Read the failure report",
|
|
480
|
+
copy: ""
|
|
481
|
+
},
|
|
482
|
+
repair: {
|
|
483
|
+
title: "Set the release gate",
|
|
484
|
+
copy: "Repair the command boundary so a long scrape cannot start from bad input or report success when nothing useful happened."
|
|
485
|
+
},
|
|
486
|
+
transfer: {
|
|
487
|
+
title: "Create the release check",
|
|
488
|
+
copy: "Turn the decision into a short checklist you can reuse before the next command-line tool ships."
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
return map[id] || { title: fallbackTitle, copy: fallbackCopy };
|
|
492
|
+
}
|
|
493
|
+
function buildBeats() {
|
|
494
|
+
var beats = [];
|
|
495
|
+
var meta;
|
|
496
|
+
if (MODULE.spot) { meta = missionMeta("spot", "Diagnose the decision", "Start with the evidence. Find the lines that show the decision before reading the explanation."); beats.push({ id: "spot", type: "spot", stage: "diagnose", title: meta.title, copy: meta.copy }); }
|
|
497
|
+
else { meta = missionMeta("diagnose", "Diagnose the decision", "Decide what kind of engineering judgment the AI exercised."); beats.push({ id: "diagnose", type: "mc", stage: "diagnose", title: meta.title, copy: meta.copy }); }
|
|
498
|
+
if (MODULE.investigate) { meta = missionMeta("inv", "Predict what breaks", "Here is the same work with the decision removed. Find where the missing safeguard fails."); beats.push({ id: "inv", type: "inv", stage: "break", title: meta.title, copy: meta.copy }); }
|
|
499
|
+
else { meta = missionMeta("break", "Predict what breaks", "Choose the breakage that explains why the decision matters."); beats.push({ id: "break", type: "mc", stage: "break", title: meta.title, copy: meta.copy }); }
|
|
500
|
+
if (MODULE.failureSim) { meta = missionMeta("crash", "Watch it break", ""); beats.push({ id: "crash", type: "crash", stage: "break", title: meta.title, copy: meta.copy }); }
|
|
501
|
+
if (MODULE.repairLab) { meta = missionMeta("repair", "Repair the design", "Edit the repair, then run the rubric check against your submission."); beats.push({ id: "repair", type: "editor", stage: "repair", title: meta.title, copy: meta.copy }); }
|
|
502
|
+
else { meta = missionMeta("repair", "Repair the design", "Choose the standard you would require before trusting the change."); beats.push({ id: "repair", type: "mc", stage: "repair", title: meta.title, copy: meta.copy }); }
|
|
503
|
+
if (MODULE.transferLab) { meta = missionMeta("transfer", "Transfer to a new situation", "Apply the same judgment to a new situation."); beats.push({ id: "transfer", type: "fields", stage: "transfer", title: meta.title, copy: meta.copy }); }
|
|
504
|
+
else { meta = missionMeta("transfer", "Transfer to a new situation", "Reuse the judgment in a new context."); beats.push({ id: "transfer", type: "mc", stage: "transfer", title: meta.title, copy: meta.copy }); }
|
|
505
|
+
return beats;
|
|
506
|
+
}
|
|
507
|
+
var BEATS = buildBeats();
|
|
508
|
+
|
|
509
|
+
function beatDone(b) {
|
|
510
|
+
if (b.type === "spot") return spotComplete();
|
|
511
|
+
if (b.type === "inv") return state.inv === MODULE.investigate.targetLine;
|
|
512
|
+
if (b.type === "crash") {
|
|
513
|
+
if (MODULE.failureSim.arbitrate) return state.arbHit;
|
|
514
|
+
return state.inv === MODULE.investigate.targetLine;
|
|
515
|
+
}
|
|
516
|
+
if (b.type === "editor") return Boolean(state.complete.repair);
|
|
517
|
+
if (b.type === "fields") return Boolean(state.complete.transfer);
|
|
518
|
+
return isRight(b.id);
|
|
519
|
+
}
|
|
520
|
+
function stageDone(stageId) {
|
|
521
|
+
return BEATS.filter(function (b) { return b.stage === stageId; }).every(beatDone);
|
|
522
|
+
}
|
|
523
|
+
function isRight(stageId) {
|
|
524
|
+
var m = stageMod(stageId);
|
|
525
|
+
return m && state.choices[stageId] != null && Boolean(m.choices[state.choices[stageId]].correct);
|
|
526
|
+
}
|
|
527
|
+
function stageMod(stageId) {
|
|
528
|
+
return MODULE[stageId === "diagnose" ? "diagnose" : stageId === "break" ? "break" : stageId === "repair" ? "repair" : "transfer"];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ---- shared pieces ----
|
|
532
|
+
function passLine(stageId) {
|
|
533
|
+
return '<div class="pass-line"><b>Pass condition</b><span>' + esc(MODULE.criteria[stageId]) + "</span></div>";
|
|
534
|
+
}
|
|
535
|
+
function unlock2(open, title, inner) {
|
|
536
|
+
if (!open) return "";
|
|
537
|
+
return '<div class="unlock"><h4>' + esc(title) + '</h4><div class="unlock-grid">' + inner + "</div></div>";
|
|
538
|
+
}
|
|
539
|
+
function ucard(label, text, html) {
|
|
540
|
+
return '<div class="ucard"><b>' + esc(label) + "</b><p>" + (html || fmt(text)) + "</p></div>";
|
|
541
|
+
}
|
|
542
|
+
function missionTone(text) {
|
|
543
|
+
if (!missionEnabled()) return text;
|
|
544
|
+
return String(text || "")
|
|
545
|
+
.replace(/\bThe AI added\b/g, "The session introduced")
|
|
546
|
+
.replace(/\bthe AI added\b/g, "the session introduced")
|
|
547
|
+
.replace(/\bAI-built\b/g, "new")
|
|
548
|
+
.replace(/\banother AI\b/g, "another")
|
|
549
|
+
.replace(/\bbefore it codes\b/g, "before work starts");
|
|
550
|
+
}
|
|
551
|
+
function evidenceDrawer(stageId, open) {
|
|
552
|
+
var lens = MODULE.lenses[stageId] || MODULE.lenses.diagnose;
|
|
553
|
+
var pills = lens.items.map(function (i) { return "<span>" + esc(i) + "</span>"; }).join("");
|
|
554
|
+
return '<details class="evidence"' + (open ? " open" : "") + '><summary>Session Evidence — the original diff</summary>' +
|
|
555
|
+
'<div class="evidence-body"><div class="lens"><b>' + esc(lens.title) + "</b>" + pills + "</div>" +
|
|
556
|
+
diffView("drawer", EVIDENCE_HTML) + "</div></details>";
|
|
557
|
+
}
|
|
558
|
+
function choicesHtml(stageId) {
|
|
559
|
+
var m = stageMod(stageId);
|
|
560
|
+
var picked = state.choices[stageId];
|
|
561
|
+
return '<div class="choices">' + m.choices.map(function (c, i) {
|
|
562
|
+
var cls = "choice";
|
|
563
|
+
if (picked === i) cls += c.correct ? " is-right" : " is-wrong";
|
|
564
|
+
else if (picked != null && m.choices[picked] && m.choices[picked].correct) cls += " is-faded";
|
|
565
|
+
var body = picked === i ? c.feedback : c.description;
|
|
566
|
+
return '<button type="button" class="' + cls + '" data-pick="' + stageId + ':' + i + '">' +
|
|
567
|
+
'<span class="key">' + (picked === i ? (c.correct ? "✓" : "✗") : String.fromCharCode(65 + i)) + "</span>" +
|
|
568
|
+
"<span><b>" + fmt(c.label) + "</b><p>" + fmt(body) + "</p></span></button>";
|
|
569
|
+
}).join("") + "</div>";
|
|
570
|
+
}
|
|
571
|
+
function checkTerm(stageId) {
|
|
572
|
+
if (!(stageId in state.choices)) return "";
|
|
573
|
+
var c = stageMod(stageId).choices[state.choices[stageId]];
|
|
574
|
+
var status = c.correct ? '<span class="t-pass">status: PASS</span>' : '<span class="t-fail">status: FAIL</span>';
|
|
575
|
+
return '<div class="term"><div class="t-line t-cmd">replay check ' + esc(stageId) + "</div>" +
|
|
576
|
+
'<div class="t-line">' + status + '</div><div class="t-line t-meta">reason: ' + fmt(c.feedback) + "</div></div>";
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ---- spot beat (multi-line blast radius) ----
|
|
580
|
+
// Targets are the lines that together carry the decision. Only targets that
|
|
581
|
+
// actually appear in this session's evidence are required, so it degrades to
|
|
582
|
+
// single-line on any diff. Backward compatible with the old targetRe.
|
|
583
|
+
function spotTargets() {
|
|
584
|
+
var defs = MODULE.spot.targets || [{ re: MODULE.spot.targetRe, note: MODULE.spot.hit }];
|
|
585
|
+
var raw = EVIDENCE_RAW.split("\n");
|
|
586
|
+
return defs.filter(function (t) {
|
|
587
|
+
return raw.some(function (l) { return new RegExp(t.re, "i").test(l); });
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
function targetIndexFor(line) {
|
|
591
|
+
var defs = spotTargets();
|
|
592
|
+
for (var i = 0; i < defs.length; i++) {
|
|
593
|
+
if (new RegExp(defs[i].re, "i").test(line)) return i;
|
|
594
|
+
}
|
|
595
|
+
return -1;
|
|
596
|
+
}
|
|
597
|
+
function spotComplete() {
|
|
598
|
+
var req = spotTargets().length;
|
|
599
|
+
// Safety net: if no target line could be located in this evidence (e.g. a
|
|
600
|
+
// generated lab whose target did not match the session diff), the beat must still
|
|
601
|
+
// be completable — one click anywhere reveals the decision and continues.
|
|
602
|
+
if (req === 0) return Boolean(state.spotHit);
|
|
603
|
+
return state.spotFound.length >= req;
|
|
604
|
+
}
|
|
605
|
+
function diffWindow() {
|
|
606
|
+
var lines = EVIDENCE_RAW.split("\n");
|
|
607
|
+
var done = spotComplete();
|
|
608
|
+
var inner = lines.map(function (l, i) {
|
|
609
|
+
var cls = "cl";
|
|
610
|
+
if (l.indexOf("diff --git") === 0 || l.indexOf("index ") === 0 || l.indexOf("---") === 0 || l.indexOf("+++") === 0) cls += " d-meta";
|
|
611
|
+
else if (l.charAt(0) === "+") cls += " d-add"; else if (l.charAt(0) === "-") cls += " d-del";
|
|
612
|
+
else if (l.indexOf("@@") === 0) cls += " d-hunk";
|
|
613
|
+
var ti = targetIndexFor(l);
|
|
614
|
+
if (ti >= 0 && state.spotFound.indexOf(ti) !== -1) cls += " cl-hit";
|
|
615
|
+
else if (state.spotPick === i && ti < 0) cls += " cl-miss";
|
|
616
|
+
return '<span class="' + cls + '" data-dline="' + i + '">' + diffLineHtml(l) + "</span>";
|
|
617
|
+
}).join("");
|
|
618
|
+
var req = spotTargets().length;
|
|
619
|
+
var flag = done ? "" : (req > 1
|
|
620
|
+
? '<span class="flag" style="color:var(--warn);border-color:#fbbf2433;background:#fbbf2415">found ' + state.spotFound.length + " of " + req + "</span>"
|
|
621
|
+
: '<span class="flag" style="color:var(--warn);border-color:#fbbf2433;background:#fbbf2415">click a line</span>');
|
|
622
|
+
return '<div class="code-win' + (done ? " done" : "") + '"><div class="code-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>' +
|
|
623
|
+
'<span class="fname">session excerpt</span>' + diffToggles("spot", flag) +
|
|
624
|
+
'</div><div class="code-body"><pre class="code' + (state.diffViews.spot === "patch" ? "" : " diff-readable") + '" data-diff-box="spot" style="width:100%">' + inner + "</pre></div></div>";
|
|
625
|
+
}
|
|
626
|
+
function spotPrompt() {
|
|
627
|
+
var base = MODULE.spot.prompt;
|
|
628
|
+
var req = spotTargets().length;
|
|
629
|
+
if (req === 0) return base.replace(/Click the line.*$/i, "") + "Read the evidence, then click any line to surface the decision.";
|
|
630
|
+
return req > 1
|
|
631
|
+
? base.replace(/Click the line.*$/i, "") + "Click each line that supports this decision or depends on it."
|
|
632
|
+
: base;
|
|
633
|
+
}
|
|
634
|
+
function spotFeedback() {
|
|
635
|
+
var lines = [];
|
|
636
|
+
// the most recent found target's note (progress), or the all-found reveal
|
|
637
|
+
if (spotComplete()) {
|
|
638
|
+
lines.push('<div class="t-line t-pass">status: PASS</div>');
|
|
639
|
+
lines.push('<div class="t-line t-meta">reason: ' + fmt(MODULE.spot.hit) + "</div>");
|
|
640
|
+
} else if (state.spotFound.length > 0) {
|
|
641
|
+
var lastTi = state.spotFound[state.spotFound.length - 1];
|
|
642
|
+
var def = spotTargets()[lastTi];
|
|
643
|
+
var req = spotTargets().length;
|
|
644
|
+
lines.push('<div class="t-line t-pass">✓ ' + fmt(def.note) + "</div>");
|
|
645
|
+
var rem = req - state.spotFound.length;
|
|
646
|
+
lines.push('<div class="t-line t-meta">' + rem + " more line" + (rem === 1 ? " carries" : "s carry") + " this decision — keep tracing.</div>");
|
|
647
|
+
} else if (state.spotPick != null) {
|
|
648
|
+
var line = EVIDENCE_RAW.split("\n")[state.spotPick] || "";
|
|
649
|
+
lines.push('<div class="t-line t-fail">status: keep looking</div>');
|
|
650
|
+
lines.push('<div class="t-line t-meta">reason: ' + fmt(missNote(line)) + "</div>");
|
|
651
|
+
}
|
|
652
|
+
if (!lines.length) return "";
|
|
653
|
+
return '<div class="term"><div class="t-line t-cmd">replay check diagnose</div>' + lines.join("") + "</div>";
|
|
654
|
+
}
|
|
655
|
+
function missNote(line) {
|
|
656
|
+
var rules = MODULE.spot.misses || [];
|
|
657
|
+
for (var i = 0; i < rules.length; i++) {
|
|
658
|
+
if (new RegExp(rules[i].re, "i").test(line)) return rules[i].note;
|
|
659
|
+
}
|
|
660
|
+
return MODULE.spot.missDefault;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ---- inv beat ----
|
|
664
|
+
function clickableCode(code) {
|
|
665
|
+
var done = state.inv === MODULE.investigate.targetLine;
|
|
666
|
+
var lines = code.split("\n").map(function (l, i) {
|
|
667
|
+
var n = i + 1;
|
|
668
|
+
var cls = "cl";
|
|
669
|
+
if (state.inv === n) cls += (n === MODULE.investigate.targetLine ? " cl-hit" : " cl-miss");
|
|
670
|
+
return '<span class="' + cls + '" data-line="' + n + '">' + (hl(l) || " ") + "</span>";
|
|
671
|
+
}).join("");
|
|
672
|
+
return '<div class="code-win' + (done ? " done" : "") + '"><div class="code-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>' +
|
|
673
|
+
'<span class="fname">' + esc(MODULE.naiveFile || "") + " — naive version</span>" +
|
|
674
|
+
(done ? "" : '<span class="flag" style="color:var(--warn);border-color:#fbbf2433;background:#fbbf2415">click a line</span>') +
|
|
675
|
+
'</div><div class="code-body"><div class="gutter">' + gutterFor(code) + '</div><pre class="code">' + lines + "</pre></div></div>";
|
|
676
|
+
}
|
|
677
|
+
function invFeedback() {
|
|
678
|
+
var inv = MODULE.investigate;
|
|
679
|
+
if (state.inv == null) return "";
|
|
680
|
+
var hitIt = state.inv === inv.targetLine;
|
|
681
|
+
var text = hitIt ? inv.hit : ((inv.misses && inv.misses[String(state.inv)]) || inv.missDefault);
|
|
682
|
+
return '<div class="term"><div class="t-line t-cmd">replay check break</div>' +
|
|
683
|
+
'<div class="t-line ' + (hitIt ? "t-pass" : "t-fail") + '">status: ' + (hitIt ? "PASS" : "FAIL") + "</div>" +
|
|
684
|
+
'<div class="t-line t-meta">reason: ' + fmt(text) + "</div></div>";
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ---- crash beat ----
|
|
688
|
+
function crashBox() {
|
|
689
|
+
var fresh = state.flash === "crash";
|
|
690
|
+
var lines = MODULE.failureSim.terminal.split("\n");
|
|
691
|
+
var inner = lines.map(function (l, i) {
|
|
692
|
+
var style = fresh ? ' style="animation-delay:' + (i * 170) + 'ms"' : "";
|
|
693
|
+
var html = esc(l).replace(/(ReferenceError[^<]*|usage alert[^<]*)/g, '<span class="err">$1</span>');
|
|
694
|
+
return '<div class="t-line"' + style + ">" + (html || " ") + "</div>";
|
|
695
|
+
}).join("");
|
|
696
|
+
return '<div class="crashbox">' + inner + "</div>";
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function arbThread() {
|
|
700
|
+
var arb = MODULE.failureSim.arbitrate;
|
|
701
|
+
if (!arb) {
|
|
702
|
+
return '<div class="term"><div class="t-line t-sum">' + fmt(MODULE.failureSim.narration || "") + "</div></div>";
|
|
703
|
+
}
|
|
704
|
+
var fresh = state.flash === "crash";
|
|
705
|
+
var introDelay = fresh ? ' style="animation-delay:' + (MODULE.failureSim.terminal.split("\n").length * 170 + 500) + 'ms"' : "";
|
|
706
|
+
var html = '<div class="thread-intro t-line"' + introDelay + ">" + esc(arb.intro) + "</div>";
|
|
707
|
+
arb.comments.forEach(function (c, i) {
|
|
708
|
+
var cls = "rc";
|
|
709
|
+
if (state.arbPick === i) cls += c.correct ? " is-right" : " is-wrong";
|
|
710
|
+
else if (state.arbHit) cls += " is-faded";
|
|
711
|
+
var initials = c.handle.split(".")[0].slice(0, 2).toUpperCase();
|
|
712
|
+
html += '<div class="t-line"' + introDelay + '><button type="button" class="' + cls + '" data-arb="' + i + '">' +
|
|
713
|
+
'<span class="avatar">' + esc(initials) + "</span>" +
|
|
714
|
+
'<span><span class="handle">' + esc(c.handle) + " commented</span><p>" + fmt(c.text) + "</p></span>" +
|
|
715
|
+
'<span class="approve">approve ▸</span></button>' +
|
|
716
|
+
(state.arbPick === i ? '<div class="verdict-note ' + (c.correct ? "good" : "bad") + '">' + fmt(c.verdict) + "</div>" : "") +
|
|
717
|
+
"</div>";
|
|
718
|
+
});
|
|
719
|
+
if (state.arbHit) {
|
|
720
|
+
html += '<div class="term"><div class="t-line t-sum">' + fmt(MODULE.failureSim.narration || "") + "</div></div>";
|
|
721
|
+
}
|
|
722
|
+
return html;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ---- review ----
|
|
726
|
+
function reviewTerm(stageId) {
|
|
727
|
+
if (state.reviewing === stageId) {
|
|
728
|
+
var names = (MODULE.reviewCriteria && MODULE.reviewCriteria[stageId]) || [];
|
|
729
|
+
var checking = names.map(function (n, i) {
|
|
730
|
+
return '<div class="t-line t-meta" style="animation-delay:' + (i * 450 + 400) + 'ms">· reading for: ' + esc(n) + "</div>";
|
|
731
|
+
}).join("");
|
|
732
|
+
return '<div class="term"><div class="t-line t-cmd">replay check ' + esc(stageId) + '</div>' +
|
|
733
|
+
'<div class="t-line t-meta">reviewer: Claude check running on your submission</div>' +
|
|
734
|
+
checking +
|
|
735
|
+
'<div class="t-line t-meta spin" style="animation-delay:' + (names.length * 450 + 600) + 'ms">judging against the pass rule <span id="elapsed-' + esc(stageId) + '"></span></div>' +
|
|
736
|
+
'<div class="t-line t-sum" style="animation-delay:' + (names.length * 450 + 2200) + 'ms">while you wait — the rule being checked: ' + esc(MODULE.takeaway) + "</div></div>";
|
|
737
|
+
}
|
|
738
|
+
var r = state.reviews[stageId];
|
|
739
|
+
if (!r) return "";
|
|
740
|
+
var animate = state.flash === stageId;
|
|
741
|
+
var d = 0;
|
|
742
|
+
function line(html) {
|
|
743
|
+
d += 1;
|
|
744
|
+
var style = animate ? ' style="animation-delay:' + (d * 110) + 'ms"' : "";
|
|
745
|
+
return '<div class="t-line"' + style + ">" + html + "</div>";
|
|
746
|
+
}
|
|
747
|
+
var out = '<div class="term">' + line('<span class="t-cmd">replay check ' + esc(stageId) + "</span>");
|
|
748
|
+
var who = r.reviewer === "claude" ? "reviewer: Claude check on your submission"
|
|
749
|
+
: r.reviewer === "heuristic" ? "reviewer: heuristic fallback (claude CLI unavailable on the server)"
|
|
750
|
+
: r.reviewer === "offline" ? "reviewer: offline pattern-match — run 'node ./src/cli.js serve' and reload for Claude checks"
|
|
751
|
+
: "reviewer: validator";
|
|
752
|
+
out += line('<span class="t-meta">' + esc(who) + "</span>");
|
|
753
|
+
(r.criteria || []).forEach(function (c) {
|
|
754
|
+
out += line(c.pass ? '<span class="t-pass">✓ ' + esc(c.note) + "</span>" : '<span class="t-fail">✗ ' + esc(c.note) + "</span>");
|
|
755
|
+
});
|
|
756
|
+
out += line('<span class="stamp ' + (r.overall === "PASS" ? "pass" : "fail") + '">' + esc(r.overall) + "</span>");
|
|
757
|
+
if (r.summary) out += line('<div class="t-sum">' + esc(r.summary) + "</div>");
|
|
758
|
+
if (r.misconception && r.overall !== "PASS") out += line('<div class="t-mis">misconception: ' + esc(r.misconception) + "</div>");
|
|
759
|
+
return out + "</div>";
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ---- beat rendering ----
|
|
763
|
+
function beatInner(b) {
|
|
764
|
+
if (b.type === "spot") {
|
|
765
|
+
return '<div class="bprompt">' + spotPrompt() + "</div>" + diffWindow() + spotFeedback() + passLine("diagnose") +
|
|
766
|
+
unlock2(beatDone(b), "Mental model unlocked",
|
|
767
|
+
ucard("Decision name", MODULE.name) + ucard("Why it appeared", MODULE.why) + ucard("Reusable rule", MODULE.takeaway));
|
|
768
|
+
}
|
|
769
|
+
if (b.type === "inv") {
|
|
770
|
+
return '<div class="sim-label" style="color:var(--warn)">Investigate — trace the mechanism yourself</div>' +
|
|
771
|
+
'<div class="bprompt">' + fmt(MODULE.investigate.prompt) + "</div>" + clickableCode(MODULE.naiveCode) + invFeedback() + passLine("break");
|
|
772
|
+
}
|
|
773
|
+
if (b.type === "crash") {
|
|
774
|
+
return '<div class="sim-label">Run — the terminal, ten seconds later</div>' + crashBox() + arbThread() +
|
|
775
|
+
unlock2(beatDone(b), "Failure mode unlocked",
|
|
776
|
+
ucard("Naive version", MODULE.naive) + ucard("What breaks", MODULE.breaks));
|
|
777
|
+
}
|
|
778
|
+
if (b.type === "editor") {
|
|
779
|
+
var code = state.answers.repair != null ? state.answers.repair : MODULE.repairLab.starter;
|
|
780
|
+
var review = state.reviews.repair;
|
|
781
|
+
var hasBlocks = Boolean(MODULE.repairLab.blocks);
|
|
782
|
+
var modeRow = hasBlocks
|
|
783
|
+
? '<div class="mode-row"><button type="button" class="mode-btn' + (state.repairMode === "asm" ? " on" : "") + '" data-mode="asm">Assemble it</button>' +
|
|
784
|
+
'<button type="button" class="mode-btn' + (state.repairMode === "type" ? " on" : "") + '" data-mode="type">Type it</button></div>'
|
|
785
|
+
: "";
|
|
786
|
+
var workspace = (hasBlocks && state.repairMode === "asm")
|
|
787
|
+
? assemblyUi()
|
|
788
|
+
: '<div class="code-win"><div class="code-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>' +
|
|
789
|
+
'<span class="fname">' + esc(MODULE.repairLab.filename || "your repair") + "</span></div>" +
|
|
790
|
+
'<textarea class="editor" id="editor-repair" spellcheck="false">' + esc(code) + "</textarea></div>";
|
|
791
|
+
return '<div class="bprompt">' + fmt(MODULE.repair.prompt) + "</div>" +
|
|
792
|
+
evidenceDrawer("repair", false) +
|
|
793
|
+
'<p class="stage-copy" style="margin-bottom:14px">' + esc(MODULE.repairLab.instructions) + "</p>" +
|
|
794
|
+
modeRow + workspace +
|
|
795
|
+
(MODULE.repairLab.solution && state.solShown
|
|
796
|
+
? '<div class="bin-label" style="color:var(--muted)">Reference option — compare it with your submission</div>' +
|
|
797
|
+
'<div class="code-win"><div class="code-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>' +
|
|
798
|
+
'<span class="fname">reference option</span></div>' +
|
|
799
|
+
'<div class="code-body"><div class="gutter">' + gutterFor(MODULE.repairLab.solution) + '</div><pre class="code">' + hl(MODULE.repairLab.solution) + "</pre></div></div>"
|
|
800
|
+
: "") +
|
|
801
|
+
'<div class="cta-row"><button class="btn-lint" data-lint="repair">replay lint · instant</button>' +
|
|
802
|
+
'<button class="cta run" data-review="repair"' + (state.reviewing ? " disabled" : "") + ">" +
|
|
803
|
+
(state.reviewing === "repair" ? "reviewing…" : "▶ replay check repair") + "</button>" +
|
|
804
|
+
(review && review.overall === "FAIL" && state.repairMode !== "asm" ? '<button class="btn-lint" data-stub="repair">stub what is missing</button>' : "") +
|
|
805
|
+
(MODULE.repairLab.solution ? '<button class="btn-lint" data-solution="1">' + (state.solShown ? "hide reference" : "show a reference solution") + "</button>" : "") +
|
|
806
|
+
"</div>" + lintTerm("repair") + reviewTerm("repair") + passLine("repair") +
|
|
807
|
+
unlock2(beatDone(b), "Better design unlocked",
|
|
808
|
+
ucard("Session version", missionTone(MODULE.aiVersion)) + ucard("More complete version", missionTone(MODULE.production)) +
|
|
809
|
+
(MODULE.patternHref ? ucard("Catalog entry", "", esc(MODULE.name) + ' joins your catalog. <a href="' + esc(MODULE.patternHref) + '">Read the full pattern page →</a>') : ""));
|
|
810
|
+
}
|
|
811
|
+
if (b.type === "fields") {
|
|
812
|
+
var review2 = state.reviews.transfer;
|
|
813
|
+
var inputs;
|
|
814
|
+
var missionTransfer = missionEnabled();
|
|
815
|
+
var transferInstructions = missionTransfer
|
|
816
|
+
? "Use the starter checklist, then edit only what you disagree with."
|
|
817
|
+
: MODULE.transferLab.instructions;
|
|
818
|
+
var starterApplied = missionTransfer && MODULE.transferLab.fields && MODULE.transferLab.fields.every(function (f) {
|
|
819
|
+
return Boolean(state.answers["transfer." + f.key]);
|
|
820
|
+
});
|
|
821
|
+
var missionStarter = missionTransfer
|
|
822
|
+
? '<div class="mission-starter"><div><b>Release check starter</b><p>' +
|
|
823
|
+
(starterApplied ? "Checklist is filled. Edit only the parts that do not fit this situation." : "One click fills the checklist. You only revise the parts that do not fit this situation.") +
|
|
824
|
+
'</p></div><button type="button" class="btn-lint" data-mission-transfer-starter>' +
|
|
825
|
+
(starterApplied ? "Starter applied" : "Use starter checklist") + "</button></div>"
|
|
826
|
+
: "";
|
|
827
|
+
if (MODULE.transferLab.fields) {
|
|
828
|
+
inputs = MODULE.transferLab.fields.map(function (f) {
|
|
829
|
+
var v = state.answers["transfer." + f.key] || "";
|
|
830
|
+
var on = FIELD_CHECKS[f.key] && FIELD_CHECKS[f.key].test(v);
|
|
831
|
+
var chips = (f.chips || []).map(function (c, ci) {
|
|
832
|
+
return '<button type="button" class="pchip" data-chip="' + esc(f.key) + ':' + ci + '">' + esc(c.length > 64 ? c.slice(0, 61) + "…" : c) + "</button>";
|
|
833
|
+
}).join("");
|
|
834
|
+
return '<label class="f-label">' + esc(f.label) + '<span class="dot-cov' + (on ? " on" : "") + '" id="dot-' + esc(f.key) + '" title="looks covered — a hint, not a verdict"></span></label>' +
|
|
835
|
+
'<textarea class="plan f-small" rows="2" data-field="' + esc(f.key) + '" placeholder="' + esc(f.ph) + '">' + esc(v) + "</textarea>" +
|
|
836
|
+
(chips ? '<div class="chip-row">' + chips + "</div>" : "");
|
|
837
|
+
}).join("");
|
|
838
|
+
} else {
|
|
839
|
+
inputs = '<textarea class="plan" id="editor-transfer" placeholder="' + esc(MODULE.transferLab.placeholder) + '">' + esc(state.answers.transfer || "") + "</textarea>";
|
|
840
|
+
}
|
|
841
|
+
return '<div class="bprompt">' + fmt(MODULE.transfer.prompt) + "</div>" +
|
|
842
|
+
'<div class="ucard" style="margin-bottom:14px"><b>New situation</b><p>' + fmt(MODULE.transfer.scenario) + "</p></div>" +
|
|
843
|
+
'<p class="stage-copy" style="margin-bottom:4px">' + esc(transferInstructions) + "</p>" +
|
|
844
|
+
missionStarter +
|
|
845
|
+
inputs +
|
|
846
|
+
'<div class="cta-row"><button class="btn-lint" data-lint="transfer">replay lint · instant</button>' +
|
|
847
|
+
'<button class="cta run" data-review="transfer"' + (state.reviewing ? " disabled" : "") + ">" +
|
|
848
|
+
(state.reviewing === "transfer" ? "reviewing…" : "▶ replay check transfer") + "</button>" +
|
|
849
|
+
(review2 && review2.overall === "FAIL" ? '<span class="cta-note">Sharpen the plan and check again.</span>' : "") +
|
|
850
|
+
"</div>" + lintTerm("transfer") + reviewTerm("transfer") + passLine("transfer") +
|
|
851
|
+
unlock2(beatDone(b), "Transfer rule unlocked",
|
|
852
|
+
ucard("Reusable rule", missionTone(MODULE.transfer.rule)) + ucard("Next practice", missionTone(MODULE.exercise)));
|
|
853
|
+
}
|
|
854
|
+
// mc fallback (generic modules)
|
|
855
|
+
var m = stageMod(b.id);
|
|
856
|
+
return (m.scenario ? '<div class="ucard" style="margin-bottom:14px"><b>New situation</b><p>' + fmt(m.scenario) + "</p></div>" : "") +
|
|
857
|
+
'<div class="bprompt">' + fmt(m.prompt) + "</div>" +
|
|
858
|
+
(b.id === "diagnose" ? evidenceDrawer("diagnose", true) : "") +
|
|
859
|
+
choicesHtml(b.id) + checkTerm(b.id) + passLine(b.stage) +
|
|
860
|
+
unlock2(beatDone(b), "Unlocked", ucard("Takeaway", MODULE.takeaway));
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function visibleBeats() {
|
|
864
|
+
var out = [];
|
|
865
|
+
for (var i = 0; i < BEATS.length; i++) {
|
|
866
|
+
out.push(BEATS[i]);
|
|
867
|
+
if (!beatDone(BEATS[i])) break;
|
|
868
|
+
}
|
|
869
|
+
return out;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function missionPanel() {
|
|
873
|
+
var eligible = /cli contract/i.test(MODULE.name || "");
|
|
874
|
+
if (!eligible) return "";
|
|
875
|
+
var toggle = '<section class="mission-strip"><div><b>Mission mode</b><span>' +
|
|
876
|
+
(eligible ? "Run this lab as a production-readiness scenario." : "No mission draft is available for this lab yet.") +
|
|
877
|
+
'</span></div><button type="button" class="mission-toggle' + (state.missionMode ? " on" : "") + '"' +
|
|
878
|
+
(eligible ? "" : " disabled") + ' data-mission-toggle>' + (state.missionMode ? "On" : "Off") + "</button></section>";
|
|
879
|
+
if (!missionEnabled()) return toggle;
|
|
880
|
+
return toggle + '<section class="mission-brief">' +
|
|
881
|
+
'<div class="eyebrow"><b>Mission brief</b> · Substack archive run</div>' +
|
|
882
|
+
'<h2>Clear the scraper for the full archive run.</h2>' +
|
|
883
|
+
'<p>The scraper downloads newsletter images into local markdown. Before a long archive run starts, verify the command fails clearly on bad input, shows usable usage, and exits non-zero so automation can stop instead of producing a false success.</p>' +
|
|
884
|
+
'<ul><li>Confirm the command contract in the session evidence.</li><li>Expose the failure a weak CLI creates.</li><li>Set a reusable release check for the next command-line tool.</li></ul>' +
|
|
885
|
+
"</section>";
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function renderStepper() {
|
|
889
|
+
var firstOpen = null;
|
|
890
|
+
STAGE_META.forEach(function (sm) { if (firstOpen === null && !stageDone(sm.id)) firstOpen = sm.id; });
|
|
891
|
+
var html = STAGE_META.map(function (sm) {
|
|
892
|
+
var done = stageDone(sm.id);
|
|
893
|
+
var cls = "step" + (sm.id === firstOpen ? " active" : "") + (done ? " done" : "");
|
|
894
|
+
return '<button type="button" class="' + cls + '" data-goto="' + sm.id + '"><span class="pip">' + (done ? "✓" : String(STAGE_META.indexOf(sm) + 1)) + '</span><span class="lbl">' + esc(sm.label) + "</span></button>";
|
|
895
|
+
});
|
|
896
|
+
document.getElementById("stepper").innerHTML = html.join('<span class="step-link"></span>');
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function renderMastery() {
|
|
900
|
+
var root = document.getElementById("mastery-root");
|
|
901
|
+
var allDone = STAGE_META.every(function (sm) { return stageDone(sm.id); });
|
|
902
|
+
if (!allDone) { root.innerHTML = ""; return; }
|
|
903
|
+
var next = (MODULE.nextPatterns || []).map(function (p) {
|
|
904
|
+
return p.href
|
|
905
|
+
? '<a class="next-card" href="' + esc(p.href) + '"><b>' + esc(p.name) + "</b><span>" + esc(p.copy) + " Enter the lab →</span></a>"
|
|
906
|
+
: '<div class="next-card"><span class="lock">LOCKED</span><b>' + esc(p.name) + "</b><span>" + esc(p.copy) + "</span></div>";
|
|
907
|
+
}).join("");
|
|
908
|
+
root.innerHTML = '<section class="mastery">' +
|
|
909
|
+
'<div class="eyebrow"><b>Lab complete</b> · this is what you carry into the next session</div>' +
|
|
910
|
+
"<h2>Reuse Notes</h2>" +
|
|
911
|
+
'<p class="sub">' + esc(MODULE.name) + " — completed after the repair and transfer checks passed.</p>" +
|
|
912
|
+
'<div class="m-grid">' +
|
|
913
|
+
'<div class="m-card"><i>Mental model</i><p>' + esc(MODULE.takeaway) + "</p></div>" +
|
|
914
|
+
'<div class="m-card"><i>Failure signature</i><p>' + esc(MODULE.artifact.failure) + "</p></div>" +
|
|
915
|
+
'<div class="m-card"><i>Completion standard</i><p>' + esc(MODULE.artifact.standard) + "</p></div>" +
|
|
916
|
+
'<div class="m-card"><i>Transfer rule</i><p>' + esc(MODULE.transfer.rule) + "</p></div>" +
|
|
917
|
+
"</div>" +
|
|
918
|
+
(MODULE.patternHref ? '<div class="next-row">' +
|
|
919
|
+
'<a class="next-card" href="' + esc(MODULE.patternHref) + '"><b>' + esc(MODULE.name) + "</b><span>Read the catalog entry →</span></a>" + next + "</div>" : "") +
|
|
920
|
+
"</section>";
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
var prevCount = -1;
|
|
924
|
+
function capturePanelScroll() {
|
|
925
|
+
var out = {};
|
|
926
|
+
document.querySelectorAll("[data-diff-box]").forEach(function (el) {
|
|
927
|
+
out[el.getAttribute("data-diff-box")] = { top: el.scrollTop, left: el.scrollLeft };
|
|
928
|
+
});
|
|
929
|
+
return out;
|
|
930
|
+
}
|
|
931
|
+
function restorePanelScroll(scrolls) {
|
|
932
|
+
Object.keys(scrolls || {}).forEach(function (id) {
|
|
933
|
+
var el = document.querySelector('[data-diff-box="' + id + '"]');
|
|
934
|
+
if (el) {
|
|
935
|
+
el.scrollTop = scrolls[id].top;
|
|
936
|
+
el.scrollLeft = scrolls[id].left;
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
function render() {
|
|
941
|
+
BEATS = buildBeats();
|
|
942
|
+
var panelScroll = prevCount !== -1 ? capturePanelScroll() : null;
|
|
943
|
+
var beats = visibleBeats();
|
|
944
|
+
var html = beats.map(function (b, i) {
|
|
945
|
+
var done = beatDone(b);
|
|
946
|
+
return '<section class="beat' + (done ? " done" : "") + '" id="beat-' + b.id + '">' +
|
|
947
|
+
'<div class="beat-eyebrow">' + (done ? "✓ " : "0" + (i + 1) + " · ") + esc(b.title) + "</div>" +
|
|
948
|
+
(b.copy && !done ? '<p class="stage-copy">' + esc(b.copy) + "</p>" : "") +
|
|
949
|
+
beatInner(b) + "</section>";
|
|
950
|
+
}).join("");
|
|
951
|
+
document.getElementById("stage-root").innerHTML = missionPanel() + html;
|
|
952
|
+
renderStepper();
|
|
953
|
+
renderMastery();
|
|
954
|
+
bind();
|
|
955
|
+
if (prevCount !== -1 && beats.length > prevCount) {
|
|
956
|
+
var newest = document.getElementById("beat-" + beats[beats.length - 1].id);
|
|
957
|
+
if (newest) setTimeout(function () { newest.scrollIntoView({ behavior: "smooth", block: "start" }); }, 150);
|
|
958
|
+
}
|
|
959
|
+
prevCount = beats.length;
|
|
960
|
+
state.flash = null;
|
|
961
|
+
if (panelScroll) requestAnimationFrame(function () { restorePanelScroll(panelScroll); });
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function bind() {
|
|
965
|
+
document.querySelectorAll("[data-mission-toggle]").forEach(function (button) {
|
|
966
|
+
button.addEventListener("click", function () {
|
|
967
|
+
state.missionMode = !state.missionMode;
|
|
968
|
+
save(); render();
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
document.querySelectorAll("[data-pick]").forEach(function (button) {
|
|
972
|
+
button.addEventListener("click", function () {
|
|
973
|
+
var parts = button.getAttribute("data-pick").split(":");
|
|
974
|
+
state.choices[parts[0]] = Number(parts[1]);
|
|
975
|
+
save(); render();
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
document.querySelectorAll("[data-dline]").forEach(function (el) {
|
|
979
|
+
el.addEventListener("click", function () {
|
|
980
|
+
if (spotComplete()) return;
|
|
981
|
+
var i = Number(el.getAttribute("data-dline"));
|
|
982
|
+
// Zero-target safety net: any click completes and reveals the decision.
|
|
983
|
+
if (spotTargets().length === 0) { state.spotHit = true; save(); render(); return; }
|
|
984
|
+
var line = EVIDENCE_RAW.split("\n")[i] || "";
|
|
985
|
+
var ti = targetIndexFor(line);
|
|
986
|
+
if (ti >= 0 && state.spotFound.indexOf(ti) === -1) {
|
|
987
|
+
state.spotFound.push(ti);
|
|
988
|
+
state.spotPick = null;
|
|
989
|
+
} else if (ti < 0) {
|
|
990
|
+
state.spotPick = i;
|
|
991
|
+
}
|
|
992
|
+
state.spotHit = spotComplete();
|
|
993
|
+
save(); render();
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
document.querySelectorAll("[data-diff-toggle]").forEach(function (button) {
|
|
997
|
+
button.addEventListener("click", function () {
|
|
998
|
+
var id = button.getAttribute("data-diff-toggle");
|
|
999
|
+
var box = document.querySelector('[data-diff-box="' + id + '"]');
|
|
1000
|
+
if (!box) return;
|
|
1001
|
+
var patchOn = state.diffViews[id] === "patch";
|
|
1002
|
+
state.diffViews[id] = patchOn ? "clean" : "patch";
|
|
1003
|
+
save();
|
|
1004
|
+
box.classList.toggle("diff-readable", patchOn);
|
|
1005
|
+
button.classList.toggle("on", !patchOn);
|
|
1006
|
+
button.setAttribute("aria-pressed", !patchOn ? "true" : "false");
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
document.querySelectorAll("[data-line]").forEach(function (el) {
|
|
1010
|
+
el.addEventListener("click", function () {
|
|
1011
|
+
if (state.inv === MODULE.investigate.targetLine) return;
|
|
1012
|
+
state.inv = Number(el.getAttribute("data-line"));
|
|
1013
|
+
if (state.inv === MODULE.investigate.targetLine) state.flash = "crash";
|
|
1014
|
+
save(); render();
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
document.querySelectorAll("[data-arb]").forEach(function (el) {
|
|
1018
|
+
el.addEventListener("click", function () {
|
|
1019
|
+
if (state.arbHit) return;
|
|
1020
|
+
var i = Number(el.getAttribute("data-arb"));
|
|
1021
|
+
state.arbPick = i;
|
|
1022
|
+
if (MODULE.failureSim.arbitrate.comments[i].correct) state.arbHit = true;
|
|
1023
|
+
save(); render();
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
document.querySelectorAll("[data-review]").forEach(function (button) {
|
|
1027
|
+
button.addEventListener("click", function () { runReview(button.getAttribute("data-review")); });
|
|
1028
|
+
});
|
|
1029
|
+
document.querySelectorAll("[data-mode]").forEach(function (button) {
|
|
1030
|
+
button.addEventListener("click", function () {
|
|
1031
|
+
var next = button.getAttribute("data-mode");
|
|
1032
|
+
// Carry assembled work into the editor so switching to type-mode never
|
|
1033
|
+
// silently reverts the submission to the naive starter.
|
|
1034
|
+
if (next === "type" && state.repairMode === "asm" && state.asm.length &&
|
|
1035
|
+
(state.answers.repair == null || state.answers.repair === MODULE.repairLab.starter)) {
|
|
1036
|
+
state.answers.repair = state.asm.map(function (i) { return MODULE.repairLab.blocks[i].code; }).join("\n");
|
|
1037
|
+
}
|
|
1038
|
+
state.repairMode = next; save(); render();
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
document.querySelectorAll("[data-add]").forEach(function (button) {
|
|
1042
|
+
button.addEventListener("click", function () {
|
|
1043
|
+
state.asm.push(Number(button.getAttribute("data-add"))); save(); render();
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
document.querySelectorAll("[data-rm]").forEach(function (el) {
|
|
1047
|
+
el.addEventListener("click", function () {
|
|
1048
|
+
state.asm.splice(Number(el.getAttribute("data-rm")), 1); save(); render();
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
1051
|
+
document.querySelectorAll("[data-chip]").forEach(function (button) {
|
|
1052
|
+
button.addEventListener("click", function () {
|
|
1053
|
+
var parts = button.getAttribute("data-chip").split(":");
|
|
1054
|
+
var f = MODULE.transferLab.fields.filter(function (x) { return x.key === parts[0]; })[0];
|
|
1055
|
+
var text = f.chips[Number(parts[1])];
|
|
1056
|
+
var cur = state.answers["transfer." + f.key] || "";
|
|
1057
|
+
state.answers["transfer." + f.key] = cur ? cur.replace(/\s*$/, "") + " " + text : text;
|
|
1058
|
+
save(); render();
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
document.querySelectorAll("[data-mission-transfer-starter]").forEach(function (button) {
|
|
1062
|
+
button.addEventListener("click", applyMissionTransferStarter);
|
|
1063
|
+
});
|
|
1064
|
+
document.querySelectorAll("[data-lint]").forEach(function (button) {
|
|
1065
|
+
button.addEventListener("click", function () { runLint(button.getAttribute("data-lint")); });
|
|
1066
|
+
});
|
|
1067
|
+
document.querySelectorAll("[data-solution]").forEach(function (button) {
|
|
1068
|
+
button.addEventListener("click", function () { state.solShown = !state.solShown; save(); render(); });
|
|
1069
|
+
});
|
|
1070
|
+
document.querySelectorAll("[data-stub]").forEach(function (button) {
|
|
1071
|
+
button.addEventListener("click", applyStubs);
|
|
1072
|
+
});
|
|
1073
|
+
document.querySelectorAll("textarea[data-field]").forEach(function (ed) {
|
|
1074
|
+
ed.addEventListener("input", function () {
|
|
1075
|
+
var key = ed.getAttribute("data-field");
|
|
1076
|
+
state.answers["transfer." + key] = ed.value; save();
|
|
1077
|
+
var dot = document.getElementById("dot-" + key);
|
|
1078
|
+
if (dot && FIELD_CHECKS[key]) dot.className = "dot-cov" + (FIELD_CHECKS[key].test(ed.value) ? " on" : "");
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
document.querySelectorAll("textarea[id^='editor-']").forEach(function (ed) {
|
|
1082
|
+
ed.addEventListener("input", function () {
|
|
1083
|
+
state.answers[ed.id.replace("editor-", "")] = ed.value; save();
|
|
1084
|
+
});
|
|
1085
|
+
ed.addEventListener("keydown", function (e) {
|
|
1086
|
+
if (e.key === "Tab") {
|
|
1087
|
+
e.preventDefault();
|
|
1088
|
+
var st = ed.selectionStart;
|
|
1089
|
+
ed.value = ed.value.slice(0, st) + " " + ed.value.slice(ed.selectionEnd);
|
|
1090
|
+
ed.selectionStart = ed.selectionEnd = st + 2;
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
document.querySelectorAll("[data-goto]").forEach(function (button) {
|
|
1095
|
+
button.addEventListener("click", function () {
|
|
1096
|
+
var stageId = button.getAttribute("data-goto");
|
|
1097
|
+
var beat = BEATS.filter(function (b) { return b.stage === stageId; })[0];
|
|
1098
|
+
var el = beat && document.getElementById("beat-" + beat.id);
|
|
1099
|
+
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
if (state.reviewing) {
|
|
1103
|
+
var span = document.getElementById("elapsed-" + state.reviewing);
|
|
1104
|
+
if (span) {
|
|
1105
|
+
var tick = setInterval(function () {
|
|
1106
|
+
if (!state.reviewing || !document.body.contains(span)) { clearInterval(tick); return; }
|
|
1107
|
+
span.textContent = "· " + Math.round((Date.now() - state.reviewStart) / 1000) + "s";
|
|
1108
|
+
}, 1000);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function getSubmission(stageId) {
|
|
1114
|
+
if (stageId === "transfer" && MODULE.transferLab && MODULE.transferLab.fields) {
|
|
1115
|
+
return MODULE.transferLab.fields.map(function (f) {
|
|
1116
|
+
return f.label + " " + (state.answers["transfer." + f.key] || "");
|
|
1117
|
+
}).join("\n");
|
|
1118
|
+
}
|
|
1119
|
+
if (stageId === "repair" && state.repairMode === "asm") {
|
|
1120
|
+
return state.asm.map(function (i) { return MODULE.repairLab.blocks[i].code; }).join("\n");
|
|
1121
|
+
}
|
|
1122
|
+
var ed = document.getElementById("editor-" + stageId);
|
|
1123
|
+
return ed ? ed.value : (state.answers[stageId] || "");
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function applyMissionTransferStarter() {
|
|
1127
|
+
if (!MODULE.transferLab || !MODULE.transferLab.fields) return;
|
|
1128
|
+
var starters = {
|
|
1129
|
+
entry_check: "Validate the required config path at the entry point before any database or file work begins.",
|
|
1130
|
+
"entry-check": "Validate the required config path at the entry point before any database or file work begins.",
|
|
1131
|
+
"test-target": "The command must validate required arguments and input paths before running business logic.",
|
|
1132
|
+
failure_mode: "A missing or stale path must not fall back to defaults, keep running, or exit with success.",
|
|
1133
|
+
"failure-mode": "A missing or stale path must not fall back to defaults, keep running, or exit with success.",
|
|
1134
|
+
error_stream: "Print usage and the specific validation error to stderr so shell automation can separate failures from normal output.",
|
|
1135
|
+
"error-stream": "Print usage and the specific validation error to stderr so shell automation can separate failures from normal output.",
|
|
1136
|
+
exit_code: "Every validation failure exits non-zero, and the release check verifies the bad-path case returns failure.",
|
|
1137
|
+
"exit-code": "Every validation failure exits non-zero, and the release check verifies the bad-path case returns failure.",
|
|
1138
|
+
regression_assertion: "Run missing-argument and bad-path cases; assert stderr explains the issue and the exit code is non-zero.",
|
|
1139
|
+
"regression-assertion": "Run missing-argument and bad-path cases; assert stderr explains the issue and the exit code is non-zero."
|
|
1140
|
+
};
|
|
1141
|
+
MODULE.transferLab.fields.forEach(function (f, i) {
|
|
1142
|
+
var fallback = [
|
|
1143
|
+
"Validate required inputs before any work starts.",
|
|
1144
|
+
"Bad input must fail clearly instead of silently continuing.",
|
|
1145
|
+
"Check stderr and a non-zero exit code for the failure case."
|
|
1146
|
+
][i] || "Record the release check this tool must pass.";
|
|
1147
|
+
state.answers["transfer." + f.key] = starters[f.key] || fallback;
|
|
1148
|
+
});
|
|
1149
|
+
save(); render();
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function assemblyUi() {
|
|
1153
|
+
var lines = state.asm.map(function (bi, pos) {
|
|
1154
|
+
return '<div class="asm-line" data-rm="' + pos + '" title="click to remove"><span>' + esc(MODULE.repairLab.blocks[bi].code) + '</span><span class="rm">remove ✕</span></div>';
|
|
1155
|
+
}).join("");
|
|
1156
|
+
var pool = MODULE.repairLab.blocks.map(function (b, i) {
|
|
1157
|
+
if (state.asm.indexOf(i) !== -1) return "";
|
|
1158
|
+
return '<button type="button" class="blk" data-add="' + i + '">' + esc(b.code) + "</button>";
|
|
1159
|
+
}).join("");
|
|
1160
|
+
return '<div class="code-win"><div class="code-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>' +
|
|
1161
|
+
'<span class="fname">' + esc(MODULE.repairLab.filename || "your repair") + ' — assembled</span></div>' +
|
|
1162
|
+
(lines || '<div class="asm-empty">Tap parts below to build the repair. Order matters. Some parts are traps.</div>') + "</div>" +
|
|
1163
|
+
'<div class="bin-label">Parts bin — choose what belongs, leave what does not</div>' + pool;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
var FIELD_CHECKS = MODULE.id === "secret-boundary"
|
|
1167
|
+
? { secret: /server|env(?!.*PUBLIC)|vault|never.*(client|browser)/i,
|
|
1168
|
+
webhook: /signature|verif|constructEvent|stripe-signature/i,
|
|
1169
|
+
validation: /validat|check|amount|schema|idempot|order state/i,
|
|
1170
|
+
verify: /test|stripe cli|replay|e2e|verify|duplicate/i }
|
|
1171
|
+
: { boundary: /client|component|'use client'|browser side/i,
|
|
1172
|
+
checks: /detect|typeof|check|navigator|permission|support/i,
|
|
1173
|
+
failures: /denied|unsupported|fallback|show|message|state/i,
|
|
1174
|
+
verify: /test|verify|e2e|deny|browser|device|firefox/i };
|
|
1175
|
+
|
|
1176
|
+
var STUBS = MODULE.id === "secret-boundary"
|
|
1177
|
+
? { route: "// TODO(route): which file owns this call now? app/api/.../route.ts",
|
|
1178
|
+
secret: "// TODO(secret): which env var name keeps the key server-only?",
|
|
1179
|
+
validation: "// TODO(validate): what counts as a bad prompt? reject it with a 400",
|
|
1180
|
+
"safe-errors": "// TODO(errors): what does the client see when the provider fails?",
|
|
1181
|
+
abuse: "// TODO(abuse): what stops a stranger scripting this endpoint all night?" }
|
|
1182
|
+
: { boundary: "// TODO(boundary): which runtime owns this file? add the directive if browser",
|
|
1183
|
+
guards: "// TODO(guards): feature-check the APIs before constructing them",
|
|
1184
|
+
unsupported: "// TODO(unsupported): what renders in a browser without SpeechRecognition?",
|
|
1185
|
+
denied: "// TODO(denied): the user blocks the mic — design that state, not a crash",
|
|
1186
|
+
verify: "// verify: how do you check the failure states before trusting this?" };
|
|
1187
|
+
|
|
1188
|
+
function runLint(stageId) {
|
|
1189
|
+
state.answers[stageId] = getSubmission(stageId);
|
|
1190
|
+
state.lints[stageId] = offlineHeuristic(stageId, state.answers[stageId]);
|
|
1191
|
+
save(); render();
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function lintTerm(stageId) {
|
|
1195
|
+
var l = state.lints[stageId];
|
|
1196
|
+
if (!l || state.reviewing === stageId || state.reviews[stageId]) return "";
|
|
1197
|
+
var lines = (l.criteria || []).map(function (c) {
|
|
1198
|
+
return '<div class="t-line">' + (c.pass
|
|
1199
|
+
? '<span class="t-pass">✓ shape present: ' + esc(c.id) + "</span>"
|
|
1200
|
+
: '<span class="t-meta">· missing shape: ' + esc(c.id) + "</span>") + "</div>";
|
|
1201
|
+
}).join("");
|
|
1202
|
+
var verdictLine = l.overall === "PASS"
|
|
1203
|
+
? '<div class="t-line t-sum">shape looks complete — worth running the rubric check.</div>'
|
|
1204
|
+
: '<div class="t-line t-meta">fill the missing shapes first, then run the rubric check.</div>';
|
|
1205
|
+
return '<div class="term lint"><div class="t-line t-cmd">replay lint ' + esc(stageId) + "</div>" +
|
|
1206
|
+
'<div class="t-line t-meta">instant pattern check — shapes only, NOT a review</div>' + lines + verdictLine + "</div>";
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function applyStubs() {
|
|
1210
|
+
var review = state.reviews.repair;
|
|
1211
|
+
if (!review) return;
|
|
1212
|
+
var code = getSubmission("repair");
|
|
1213
|
+
var added = 0;
|
|
1214
|
+
(review.criteria || []).forEach(function (c) {
|
|
1215
|
+
if (!c.pass && STUBS[c.id] && code.indexOf("TODO(" + c.id + ")") === -1 && code.indexOf(STUBS[c.id]) === -1) {
|
|
1216
|
+
code += "\n" + STUBS[c.id];
|
|
1217
|
+
added += 1;
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
if (added) { state.answers.repair = code; save(); render(); }
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function runReview(stageId) {
|
|
1224
|
+
var submission = getSubmission(stageId);
|
|
1225
|
+
state.answers[stageId] = submission;
|
|
1226
|
+
delete state.lints[stageId];
|
|
1227
|
+
state.reviewing = stageId; state.reviewStart = Date.now(); save(); render();
|
|
1228
|
+
fetch("/api/review", {
|
|
1229
|
+
method: "POST",
|
|
1230
|
+
headers: { "content-type": "application/json" },
|
|
1231
|
+
body: JSON.stringify({ stage: stageId, submission: submission, moduleId: MODULE.id, rubric: MODULE.rubric || null })
|
|
1232
|
+
}).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; })
|
|
1233
|
+
.then(function (result) {
|
|
1234
|
+
if (!result) result = offlineHeuristic(stageId, submission);
|
|
1235
|
+
state.reviewing = null;
|
|
1236
|
+
state.reviews[stageId] = result;
|
|
1237
|
+
state.complete[stageId] = result.overall === "PASS";
|
|
1238
|
+
state.flash = stageId;
|
|
1239
|
+
save(); render();
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function offlineHeuristic(stageId, submission) {
|
|
1244
|
+
if (MODULE.id === "secret-boundary") {
|
|
1245
|
+
var sChecks = stageId === "repair"
|
|
1246
|
+
? [["Server route owns the call", /app\/api|route\.(ts|js)|NextResponse|export (async )?function (POST|GET)/.test(submission), true],
|
|
1247
|
+
["Secret is server-only", !/NEXT_PUBLIC/.test(submission) && /process\.env\./.test(submission), true],
|
|
1248
|
+
["Input validation", /validat|typeof|\.length|schema|zod|400|invalid/i.test(submission), true],
|
|
1249
|
+
["Safe errors", /try|catch|status\(5|generic/i.test(submission), false],
|
|
1250
|
+
["Abuse limits", /rate|limit|429|max_tokens|cap|size/i.test(submission), false]]
|
|
1251
|
+
: [["Secret server-side", /server|env|never.*(client|browser)/i.test(submission), true],
|
|
1252
|
+
["Webhook verified", /signature|constructEvent|stripe-signature|verif/i.test(submission), true],
|
|
1253
|
+
["Validation", /validat|check|amount|schema|before marking/i.test(submission), true],
|
|
1254
|
+
["Idempotency", /idempoten|replay|duplicate/i.test(submission), false],
|
|
1255
|
+
["Verification", /test|verify|stripe cli|e2e/i.test(submission), false]];
|
|
1256
|
+
return heuristicResult(sChecks);
|
|
1257
|
+
}
|
|
1258
|
+
var checks = stageId === "repair"
|
|
1259
|
+
? [["Client boundary", /['"]use client['"]|ssr:\s*false/.test(submission), true],
|
|
1260
|
+
["Capability guards", /typeof window|in window|\?\?|\|\||navigator\./.test(submission), true],
|
|
1261
|
+
["Unsupported state", /unsupported|not supported|fallback/i.test(submission), true],
|
|
1262
|
+
["Permission denial", /denied|permission|catch/i.test(submission), false],
|
|
1263
|
+
["Verification", /test|verify|check\b/i.test(submission), false]]
|
|
1264
|
+
: [["Boundary isolation", /client (component|boundary)|['"]use client['"]|isolate/i.test(submission), true],
|
|
1265
|
+
["Capability checks", /detect|capabilit|typeof|in navigator|permissions/i.test(submission), true],
|
|
1266
|
+
["Failure states", /denied|unsupported|fallback|error state/i.test(submission), true],
|
|
1267
|
+
["Ownership reasoning", /runtime|server|render|ownership|boundary/i.test(submission), false],
|
|
1268
|
+
["Verification", /test|verify|device|browser/i.test(submission), false]];
|
|
1269
|
+
return heuristicResult(checks);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function heuristicResult(checks) {
|
|
1273
|
+
var criteria = checks.map(function (c) { return { id: c[0], pass: c[1], note: (c[1] ? "Detected: " : "Missing: ") + c[0] }; });
|
|
1274
|
+
var requiredOk = checks.filter(function (c) { return c[2]; }).every(function (c) { return c[1]; });
|
|
1275
|
+
var optionalOk = checks.filter(function (c) { return !c[2]; }).some(function (c) { return c[1]; });
|
|
1276
|
+
return { criteria: criteria, overall: requiredOk && optionalOk ? "PASS" : "FAIL",
|
|
1277
|
+
summary: "Offline pattern-match only. Run 'node ./src/cli.js serve' and reload for Claude checks.",
|
|
1278
|
+
misconception: null, reviewer: "offline" };
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
document.getElementById("reset").addEventListener("click", function () {
|
|
1282
|
+
localStorage.removeItem(KEY); location.reload();
|
|
1283
|
+
});
|
|
1284
|
+
render();
|
|
1285
|
+
`;
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
function diffToggles(id, extra = "") {
|
|
1290
|
+
return '<span class="code-tools">' +
|
|
1291
|
+
'<button type="button" class="view-toggle" aria-pressed="false" data-diff-toggle="' + id + '">Patch</button>' +
|
|
1292
|
+
extra +
|
|
1293
|
+
"</span>";
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function diffView(id, html) {
|
|
1297
|
+
return '<div class="code-win"><div class="code-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>' +
|
|
1298
|
+
'<span class="fname">session excerpt</span>' + diffToggles(id) +
|
|
1299
|
+
'</div><pre class="diff diff-readable" data-diff-box="' + id + '">' + html + "</pre></div>";
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function diffLineHtml(line) {
|
|
1303
|
+
if (line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++")) {
|
|
1304
|
+
return '<span class="d-prefix"></span><span class="d-code">' + escapeHtml(line) + "</span>";
|
|
1305
|
+
}
|
|
1306
|
+
if (line.startsWith("+") || line.startsWith("-")) {
|
|
1307
|
+
return '<span class="d-prefix">' + escapeHtml(line.slice(0, 1)) + '</span><span class="d-code">' + (escapeHtml(line.slice(1)) || " ") + "</span>";
|
|
1308
|
+
}
|
|
1309
|
+
return '<span class="d-prefix"></span><span class="d-code">' + (escapeHtml(line) || " ") + "</span>";
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function renderDiff(evidence) {
|
|
1313
|
+
return String(evidence)
|
|
1314
|
+
.split("\n")
|
|
1315
|
+
.map((line) => {
|
|
1316
|
+
if (line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++")) {
|
|
1317
|
+
return `<span class="d-meta">${diffLineHtml(line)}</span>`;
|
|
1318
|
+
}
|
|
1319
|
+
if (line.startsWith("+")) return `<span class="d-add">${diffLineHtml(line)}</span>`;
|
|
1320
|
+
if (line.startsWith("-")) return `<span class="d-del">${diffLineHtml(line)}</span>`;
|
|
1321
|
+
if (line.startsWith("@@")) return `<span class="d-hunk">${diffLineHtml(line)}</span>`;
|
|
1322
|
+
return `<span class="d-line">${diffLineHtml(line)}</span>`;
|
|
1323
|
+
})
|
|
1324
|
+
.join("\n");
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function escapeHtml(value) {
|
|
1328
|
+
return String(value)
|
|
1329
|
+
.replaceAll("&", "&")
|
|
1330
|
+
.replaceAll("<", "<")
|
|
1331
|
+
.replaceAll(">", ">")
|
|
1332
|
+
.replaceAll('"', """);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function hash(value) {
|
|
1336
|
+
let h = 0;
|
|
1337
|
+
for (let i = 0; i < value.length; i += 1) h = (h * 31 + value.charCodeAt(i)) >>> 0;
|
|
1338
|
+
return h.toString(16);
|
|
1339
|
+
}
|