oxedyne/daimond/dev/verify_twodepth.mjs
52.8 KiB, 1 run
created by r2519314175:767, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // verify_twodepth.mjs — the two-depth answer, and the streaming that is the whole |
| 2 | // point of it. |
| 3 | // |
| 4 | // A model can now fold part of its answer away in the answer itself: a |
| 5 | // `<details>` in the markdown, no tool involved. The tool it replaces, `say`, did |
| 6 | // not stream at all — an unchanging spinner for one to three minutes on a hard |
| 7 | // question, and then the whole reply at once. So THE STREAMING IS THE FEATURE. |
| 8 | // A fold that renders beautifully and stops the text arriving is a regression, |
| 9 | // however well it folds, and most of what is asserted below is about the stream |
| 10 | // rather than about the fold. |
| 11 | // |
| 12 | // What is asserted, and why each one rather than the next thing: |
| 13 | // |
| 14 | // 1. THE KEY, AGAINST THE FIXTURE ON DISK. `dev/fixtures/fold_keys.json` is the |
| 15 | // one name the Rust and JS halves must agree on (dev/CONTRACT_FOLD.md §2). |
| 16 | // Every case in it is READ FROM THE FILE and driven through the shipped |
| 17 | // `DaimondRender.foldScan` in the real page — not transcribed into this |
| 18 | // file, so a case added to the fixture cannot silently stop being checked, |
| 19 | // and a case changed cannot silently start agreeing with a copy. |
| 20 | // |
| 21 | // 2. THE ANSWER ABOVE THE FOLD STREAMS. Sampled every frame of a real turn |
| 22 | // against the mock provider: the text above the fold is complete on screen |
| 23 | // while the fold's own body is still filling, and the body grows across |
| 24 | // frames rather than appearing whole at the end. This is the check the |
| 25 | // feature exists for. |
| 26 | // |
| 27 | // 3. THE CONTROL IS MADE ONCE. A `MutationObserver` counts every |
| 28 | // `details.md-fold` ever inserted into the bubble across the whole turn, and |
| 29 | // the survivor is stamped so its identity can be read back. One insertion, |
| 30 | // not one per frame: a fold rebuilt per frame is a fold that snaps shut |
| 31 | // sixty times a second and takes the reader's place in it with it. |
| 32 | // |
| 33 | // 4. A CLOSED FOLD'S BODY IS NOT VISIBLE — AND A RECT WITH AREA IS NOT EVIDENCE. |
| 34 | // A closed `<details>` in Chrome puts its contents behind |
| 35 | // `content-visibility: hidden`, and a skipped subtree KEEPS ITS LAST LAYOUT: |
| 36 | // `getBoundingClientRect()` on a paragraph inside a closed fold comes back |
| 37 | // with real width and height, sitting below the one-line fold that is |
| 38 | // supposedly containing it. Measured that way the check would pass on a fold |
| 39 | // that never closes. So "shown" is two geometric facts together — the |
| 40 | // content's rect lies INSIDE the fold's own rect, and the point at its |
| 41 | // centre hit-tests to something inside the fold — and it is asserted BOTH |
| 42 | // ways, closed and then open, so a measurement that can only ever say "no" |
| 43 | // cannot pass for a proof. |
| 44 | // |
| 45 | // 5. THE OPEN SET REACHES WASM. `DaimondApp.set_open_folds` is wrapped on its |
| 46 | // prototype and the real one still called, so what is recorded is what the |
| 47 | // engine was actually handed. Opening a fold has to put that fold's key into |
| 48 | // it. (What the ENGINE then does with the set is the Rust half's, and is not |
| 49 | // asserted here.) |
| 50 | // |
| 51 | // 6. A FOLD WITH NOTHING ABOVE IT IS DRAWN OPEN. `say` could refuse an empty |
| 52 | // summary — src/tools.rs calls it "the one failure worth refusing", because |
| 53 | // it draws a fold the user must open to discover says nothing. Markup cannot |
| 54 | // be refused, so the renderer takes the decision instead. |
| 55 | // |
| 56 | // 7. A FENCED FOLD IS NOT A FOLD. A `<details>` inside a code fence is a model |
| 57 | // SHOWING markup. No control is drawn and the angle brackets stay on screen. |
| 58 | // |
| 59 | // 8. NOTHING EXECUTABLE RIDES IN ON THE LABEL. The summary is a NEW |
| 60 | // `innerHTML` sink taking model text: the fold's control is built by hand, |
| 61 | // so the label does not travel through `md` with the rest of the message. |
| 62 | // It goes through the same sanitiser, and this is the check that says so |
| 63 | // rather than the comment beside it. |
| 64 | // |
| 65 | // 9. A FOLD INSIDE A FOLD TAKES NO KEY. The renderer nests correctly for free, |
| 66 | // because the browser's parser does; what has to be proved is that the KEY |
| 67 | // does not, since only a top-level fold takes an ordinal |
| 68 | // (CONTRACT_FOLD.md §8) and one extra here would rename every fold after it |
| 69 | // in the engine's eyes. |
| 70 | // |
| 71 | // 12. PROSE FOLLOWED BY A TOOL CALL IS WORKING, NOT AN ANSWER. The fold governs the |
| 72 | // answer; this governs everything else the model says, which on a twenty-call turn is |
| 73 | // most of what a reader sees. The run after the LAST call is untouched and is the reply. |
| 74 | // |
| 75 | // 11. THE LABEL IS LEGIBLE. Measured against the ANSWER IT SITS BESIDE, not |
| 76 | // against a number written here: same size, same ink. Every other check in |
| 77 | // this file passes on a fold the reader never finds, which is what happened |
| 78 | // -- see 11 below for whose screen it happened on. |
| 79 | // |
| 80 | // 10. THE READER'S CHOICE SURVIVES A RELOAD. The key is an ordinal and a label |
| 81 | // and carries no message identity, which is exactly what makes this possible |
| 82 | // without touching the stored chat schema. |
| 83 | // |
| 84 | // EACH CHECK IS PROVED AGAINST BROKEN CODE FIRST. `--break <name>` serves a |
| 85 | // deliberately damaged copy of a real file to the real page (through |
| 86 | // `page.route`), and the run is then expected to FAIL. A break whose anchor does |
| 87 | // not appear exactly once aborts rather than passing quietly. |
| 88 | // |
| 89 | // node dev/verify_twodepth.mjs --break keysep # 1: the key's separator |
| 90 | // node dev/verify_twodepth.mjs --break fencedfold # 1,7: a fenced fold counted |
| 91 | // node dev/verify_twodepth.mjs --break nestordinal # 1,9: a nested fold counted |
| 92 | // node dev/verify_twodepth.mjs --break nostream # 2: drawn only at the end |
| 93 | // node dev/verify_twodepth.mjs --break perframe # 3: the control rebuilt |
| 94 | // node dev/verify_twodepth.mjs --break holdforever # 1: the hold-back never let go |
| 95 | // node dev/verify_twodepth.mjs --break flicker # 1: the half-written fold drawn |
| 96 | // node dev/verify_twodepth.mjs --break alwaysopen # 4: a fold that never shuts |
| 97 | // node dev/verify_twodepth.mjs --break bodygone # 4: a body that never shows |
| 98 | // node dev/verify_twodepth.mjs --break openbroken # 5: the gesture not recorded |
| 99 | // node dev/verify_twodepth.mjs --break rawlabel # 8: the label as live markup |
| 100 | // node dev/verify_twodepth.mjs --break bareclosed # 6: nothing above it, shut |
| 101 | // node dev/verify_twodepth.mjs --break barequiet # 6: the decision left unsaid |
| 102 | // node dev/verify_twodepth.mjs --break noreload # 10: the choice not restored |
| 103 | // node dev/verify_twodepth.mjs --break mutedlabel # 11: the label as a caption |
| 104 | // node dev/verify_twodepth.mjs --break loudworking # 12: narration drawn as an answer |
| 105 | // node dev/verify_twodepth.mjs # and then, clean |
| 106 | // |
| 107 | // eval "$(bash dev/world.sh 3 --up)" |
| 108 | // node dev/verify_twodepth.mjs |
| 109 | // |
| 110 | // BRING THE WORLD DOWN AND UP BETWEEN RUNS. Measured on 2026-08-23: a second run against a |
| 111 | // world already used reported 22 of 34 failed, and the same run against a freshly restarted |
| 112 | // world reported 34 of 34. The mock keeps state across a run -- its call-id sequence and the |
| 113 | // tool-round arithmetic it does over a conversation -- so a re-run is not a repeat. It cost an |
| 114 | // hour of disbelieving a break that was working perfectly, and it is the fixture-order trap |
| 115 | // this file already warns about, wearing a different coat. |
| 116 | // |
| 117 | // bash dev/world.sh 3 --down && eval "$(bash dev/world.sh 3 --up)" |
| 118 | // |
| 119 | // Needs dev/serve.mjs and dev/mockllm.mjs. No gateway and no real provider: the |
| 120 | // mock is told exactly what to stream, so nothing here spends anything. |
| 121 | import fs from 'node:fs'; |
| 122 | import path from 'node:path'; |
| 123 | import { fileURLToPath } from 'node:url'; |
| 124 | import { open, chat, shot, scratch, errors, signInAs } from './harness.mjs'; |
| 125 | |
| 126 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 127 | const WWW = path.join(HERE, '..', 'www'); |
| 128 | |
| 129 | const BREAK = (() => { |
| 130 | const i = process.argv.indexOf('--break'); |
| 131 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 132 | })(); |
| 133 | |
| 134 | const PROFILE = scratch('pw', 'twodepth' + (BREAK ? '-' + BREAK : '')); |
| 135 | fs.rmSync(PROFILE, { recursive: true, force: true }); |
| 136 | |
| 137 | const ok = [], bad = []; |
| 138 | const check = (name, pass, detail) => { |
| 139 | (pass ? ok : bad).push(name); |
| 140 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail != null ? ' — ' + detail : '')); |
| 141 | }; |
| 142 | |
| 143 | // ── The breaks ─────────────────────────────────────────────────────── |
| 144 | const BREAKS = { |
| 145 | // The one name both languages must agree on, spelled differently. Nothing |
| 146 | // visible changes; only the fixture notices, which is what the fixture is for. |
| 147 | keysep: [{ |
| 148 | file: 'js/render.js', |
| 149 | find: "\t\t\tf.key = i + ':' + f.summary;", |
| 150 | with: "\t\t\tf.key = i + '|' + f.summary;", |
| 151 | }], |
| 152 | // Fenced code read as markup: a model showing a reader how to write a fold |
| 153 | // has the demonstration folded away instead. |
| 154 | fencedfold: [{ |
| 155 | file: 'js/render.js', |
| 156 | find: '\t\t\tif (inRanges(fenced, m.index)) continue;', |
| 157 | with: '\t\t\tif (false) continue;', |
| 158 | }], |
| 159 | // THE REGRESSION THIS FEATURE EXISTS TO AVOID: nothing on screen until the |
| 160 | // generation ends, which is what `say` did and what made it worth replacing. |
| 161 | nostream: [{ |
| 162 | file: 'js/daimond.js', |
| 163 | find: '\t\tvar pinned = nearBottom();\n\t\tdrawAsst();\n' |
| 164 | + '\t\tif (pinned) chatOutput.scrollTop = chatOutput.scrollHeight;\n' |
| 165 | + '\t}\n\tfunction appendAssistantText(text) {', |
| 166 | with: '\t\tvar pinned = nearBottom();\n' |
| 167 | + '\t\tif (pinned) chatOutput.scrollTop = chatOutput.scrollHeight;\n' |
| 168 | + '\t}\n\tfunction appendAssistantText(text) {', |
| 169 | }], |
| 170 | // The control rebuilt every frame — the obvious implementation, and the one |
| 171 | // that loses the reader's open fold and their place in it sixty times a second. |
| 172 | perframe: [{ |
| 173 | file: 'js/daimond.js', |
| 174 | find: '\t\t\tif (!cur || cur.id !== id) {', |
| 175 | with: '\t\t\tif (true) {', |
| 176 | }], |
| 177 | // The label put on screen as markup. It is the one place model text reaches |
| 178 | // `innerHTML` without going through `md` first, and it is invisible from |
| 179 | // anywhere but here. |
| 180 | rawlabel: [{ |
| 181 | file: 'js/daimond.js', |
| 182 | find: '\t\tsum.innerHTML = DaimondRender.sanitize(seg.label);', |
| 183 | with: '\t\tsum.innerHTML = seg.label;', |
| 184 | }], |
| 185 | // The label drawn as a caption again: muted, and a size below the answer it |
| 186 | // belongs to. This is the state the owner met on 2026-08-23, and it is the one |
| 187 | // shape of this feature that nothing could see -- every behavioural check above |
| 188 | // passes on a fold nobody can find. |
| 189 | mutedlabel: [{ |
| 190 | file: 'css/app.css', |
| 191 | find: 'details.md-fold > summary {\n\tpadding: 4px 0;\n\tcolor: var(--text-primary);' |
| 192 | + '\n\tfont-size: var(--fs-base);', |
| 193 | with: 'details.md-fold > summary {\n\tpadding: 4px 0;\n\tcolor: var(--text-muted);' |
| 194 | + '\n\tfont-size: var(--fs-sm);', |
| 195 | }], |
| 196 | // The seam placed on ANY answer long enough, which is FOLD-ALL by another |
| 197 | // route: the reader is handed a closed control and nothing to read beside it. |
| 198 | // dev/PROMPT_NOTES.md §5 measured a wording that did this 8 times in 8. |
| 199 | seamall: [{ |
| 200 | file: 'js/render.js', |
| 201 | find: '\tvar SEAM_LEAD_MIN = 40;', |
| 202 | with: '\tvar SEAM_LEAD_MIN = 0;', |
| 203 | }], |
| 204 | // The blank line after the summary dropped. CONTRACT_FOLD.md §1: without it the |
| 205 | // element is one block of raw HTML and every heading inside it reaches the |
| 206 | // reader as literal hashes. Invisible from anywhere but the fixture. |
| 207 | seamblank: [{ |
| 208 | file: 'js/render.js', |
| 209 | find: "'\\n\\n<details>\\n<summary>' + head.sum + '</summary>\\n\\n'", |
| 210 | with: "'\\n\\n<details>\\n<summary>' + head.sum + '</summary>\\n'", |
| 211 | }], |
| 212 | // Prose before a tool call left as an ANSWER, at full weight in the thread. This is the |
| 213 | // state the owner met on 2026-08-23: twenty paragraphs of "let me pin the line numbers" |
| 214 | // and "de.js is done cleanly", each a sentence or two and so each exempt from FOLD_NOTE |
| 215 | // by its own last rule. The fold was working and was pointed at the wrong text. |
| 216 | loudworking: [{ |
| 217 | file: 'js/daimond.js', |
| 218 | find: "\t\t// See `demoteToWorking`.\n\t\tdemoteToWorking();", |
| 219 | with: "\t\t// See `demoteToWorking`.\n\t\tfinalizeAssistant();", |
| 220 | }], |
| 221 | // A nested fold given an ordinal of its own, which is what the contract's |
| 222 | // Amendment 1 forbids: the engine counts only top-level folds, so every key |
| 223 | // after a nested one would name a different fold in each half. |
| 224 | nestordinal: [{ |
| 225 | file: 'js/render.js', |
| 226 | find: '\t\t\tif (top) found.push(fold);', |
| 227 | with: '\t\t\tfound.push(fold);', |
| 228 | }], |
| 229 | // The hold-back removed, so the browser's own half-formed disclosure is drawn |
| 230 | // for the few frames between `<details>` and `</summary>` and then replaced. |
| 231 | // This is not hypothetical: it is what this file caught, on 2026-08-21. |
| 232 | flicker: [{ |
| 233 | file: 'js/render.js', |
| 234 | find: '\tfunction foldPending(rest) {\n', |
| 235 | with: '\tfunction foldPending(rest) {\n\t\treturn false;\n', |
| 236 | }], |
| 237 | // A fold that is never shut, so "closed hides its body" has nothing to measure. |
| 238 | alwaysopen: [{ |
| 239 | file: 'js/daimond.js', |
| 240 | find: '\t\td.open = !!_openFolds[seg.key];', |
| 241 | with: '\t\td.open = true;', |
| 242 | }], |
| 243 | // The other half of the same check: a body that is never shown. Without this |
| 244 | // break, "closed is hidden" could be passing on a measurement that always |
| 245 | // answers hidden. |
| 246 | bodygone: [{ |
| 247 | file: 'css/app.css', |
| 248 | find: 'details.md-fold > .md-fold-body > :last-child { margin-bottom: 0; }', |
| 249 | with: 'details.md-fold > .md-fold-body > :last-child { margin-bottom: 0; }\n' |
| 250 | + 'details.md-fold > .md-fold-body { display: none; }', |
| 251 | }], |
| 252 | // The gesture that manages the reader's screen no longer manages the model's |
| 253 | // working set: the fold opens, and the engine is told nothing. |
| 254 | openbroken: [{ |
| 255 | file: 'js/daimond.js', |
| 256 | find: '\t\tif (el.open) _openFolds[k] = 1; else delete _openFolds[k];', |
| 257 | with: '\t\tif (el.open) { /* broken */ } else { /* broken */ }', |
| 258 | }], |
| 259 | // A fold with nothing above it, drawn shut: a whole answer behind a control |
| 260 | // the reader has to press to find out there was nothing else. |
| 261 | bareclosed: [{ |
| 262 | file: 'js/daimond.js', |
| 263 | find: '\t\tif ((seg.open || headless) && !(seg.key in _openFolds)) _openFolds[seg.key] = 1;', |
| 264 | with: '\t\tif (seg.open && !(seg.key in _openFolds)) _openFolds[seg.key] = 1;', |
| 265 | }], |
| 266 | // The fold drawn open for the right reason but not SAYING so, which is how a |
| 267 | // decision quietly becomes a coincidence nobody can find again. |
| 268 | barequiet: [{ |
| 269 | file: 'js/daimond.js', |
| 270 | find: "\t\tif (headless) d.classList.add('md-fold-bare');", |
| 271 | with: "\t\tif (false) d.classList.add('md-fold-bare');", |
| 272 | }], |
| 273 | // The hold-back never released, so a turn that died half way through a |
| 274 | // `<summary>` shows the reader nothing where its last words should be. |
| 275 | holdforever: [{ |
| 276 | file: 'js/render.js', |
| 277 | find: '\t\t\tif (!settled && /<details/i.test(tail)) {', |
| 278 | with: '\t\t\tif (/<details/i.test(tail)) {', |
| 279 | }], |
| 280 | // The stored set never read back, so every reload shuts every fold. |
| 281 | noreload: [{ |
| 282 | file: 'js/daimond.js', |
| 283 | find: "\t\t// Before a single fold is drawn: which of them this chat's reader had open.\n" |
| 284 | + '\t\tloadTextFolds();\n', |
| 285 | with: '', |
| 286 | }], |
| 287 | }; |
| 288 | |
| 289 | if (BREAK && !BREAKS[BREAK]) { |
| 290 | console.error(`unknown break '${BREAK}'; one of: ${Object.keys(BREAKS).join(', ')}`); |
| 291 | process.exit(2); |
| 292 | } |
| 293 | |
| 294 | /// `src` with `spec` applied, or a hard stop. |
| 295 | function damaged(src, spec) { |
| 296 | const n = src.split(spec.find).length - 1; |
| 297 | if (n !== 1) { |
| 298 | console.error(`break '${BREAK}': the anchor appears ${n} times in ${spec.file}, ` |
| 299 | + 'so nothing was broken and the run below would prove nothing.'); |
| 300 | process.exit(2); |
| 301 | } |
| 302 | return src.replace(spec.find, spec.with); |
| 303 | } |
| 304 | |
| 305 | const TYPE = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript' }; |
| 306 | |
| 307 | /// The damaged files, ONE BODY PER FILE: Playwright hands a request to the LAST |
| 308 | /// route registered for its URL, so two routes on one file ship only the second. |
| 309 | function damagedFiles() { |
| 310 | const byFile = new Map(); |
| 311 | for (const spec of (BREAKS[BREAK] || [])) { |
| 312 | const src = byFile.has(spec.file) ? byFile.get(spec.file) |
| 313 | : fs.readFileSync(path.join(WWW, spec.file), 'utf8'); |
| 314 | byFile.set(spec.file, damaged(src, spec)); |
| 315 | } |
| 316 | return byFile; |
| 317 | } |
| 318 | |
| 319 | async function serveBreaks(page) { |
| 320 | if (!BREAK) return; |
| 321 | for (const [file, body] of damagedFiles()) { |
| 322 | const type = TYPE[path.extname(file)] || 'text/plain'; |
| 323 | await page.route('**/' + file, r => r.fulfill({ status: 200, contentType: type, body })); |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | // ── The fixture, read from disk ────────────────────────────────────── |
| 328 | // |
| 329 | // Not transcribed. A lane that believes a case is wrong reports it; a lane that |
| 330 | // copies the cases into its own source has stopped testing the pin. |
| 331 | const FIXTURE = path.join(HERE, 'fixtures', 'fold_keys.json'); |
| 332 | const fixture = JSON.parse(fs.readFileSync(FIXTURE, 'utf8')); |
| 333 | // The SEAM's own fixture, the pin on the other thing the two languages must agree on: |
| 334 | // what a `Fold:` line becomes. dev/CONTRACT_FOLD.md §15. |
| 335 | const SEAMFIX = path.join(HERE, 'fixtures', 'fold_seam.json'); |
| 336 | const seamfix = JSON.parse(fs.readFileSync(SEAMFIX, 'utf8')); |
| 337 | |
| 338 | // ── What the model is told to stream ───────────────────────────────── |
| 339 | // |
| 340 | // The mock splits its reply on single spaces and sends one word per frame, so |
| 341 | // these are written with single spaces and their newlines ride inside the words. |
| 342 | // Every label is different, because two folds with the same ordinal and the same |
| 343 | // label share a key ON PURPOSE (CONTRACT_FOLD.md §2) and a check that wants to |
| 344 | // name one fold needs to have named one fold. |
| 345 | const BODY = 'The cache was cold because the deploy dropped the warmed image and the ' |
| 346 | + 'replacement came up empty. Nothing was lost. The first request after a deploy ' |
| 347 | + 'now pays for the fill, every request after it does not, and the window in which ' |
| 348 | + 'that is true is about ninety seconds on the current instance size. There is a ' |
| 349 | + 'longer story about why the warmer runs after the cutover rather than before it, ' |
| 350 | + 'which is the part worth changing.'; |
| 351 | |
| 352 | // `open`, so the body is on screen while it fills and the stream can be MEASURED. |
| 353 | // A closed fold's contents are not rendered, and "did the text grow" asked of an |
| 354 | // unrendered subtree is a question with no answer. |
| 355 | const STREAMED = 'Short answer: the cache was cold.\n\n' |
| 356 | + '<details open>\n<summary>The long version</summary>\n\n' + BODY + '\n\n</details>\n'; |
| 357 | const SHUT = 'It is fixed now.\n\n' |
| 358 | + '<details>\n<summary>What was actually wrong</summary>\n\n' + BODY + '\n\n</details>\n'; |
| 359 | const BARE = '<details>\n<summary>Everything I have</summary>\n\n' + BODY + '\n\n</details>\n'; |
| 360 | const FENCED = 'Write it like this:\n\n```html\n<details>\n<summary>A label</summary>\n\n' |
| 361 | + 'The detail.\n\n</details>\n```\n'; |
| 362 | // Never touched by this run, so the reload check has something that must come |
| 363 | // back SHUT. Without one, "the reader's choice survived" would also be true of a |
| 364 | // build that simply opened everything. |
| 365 | const ALONE = 'And another thing.\n\n' |
| 366 | + '<details>\n<summary>Left alone</summary>\n\n' + BODY + '\n\n</details>\n'; |
| 367 | // The whole of an attack, in the LABEL rather than in the body, because the label |
| 368 | // is the part this change hands to `innerHTML` on its own. The image is the one |
| 369 | // that really fires: an `onerror` that has not fired yet is not an `onerror` that |
| 370 | // was stripped, and the two look identical at zero milliseconds. |
| 371 | const NASTY = 'Careful now.\n\n<details>\n<summary>Mind ' |
| 372 | + '<img src="/no-such-image-7731.png" onerror="window.__pwned=1"> ' |
| 373 | + '<script>window.__pwned=1;<\/script> ' |
| 374 | + '<a href="javascript:window.__pwned=1">the</a> gap</summary>\n\n' |
| 375 | + 'The body.\n\n</details>\n'; |
| 376 | |
| 377 | // ── What the model writes now, which is one line ───────────────────── |
| 378 | // |
| 379 | // Three wordings by three authors asked for the `<details>` and got it 5 times in |
| 380 | // 76 (dev/PROMPT_NOTES.md §5, dev/REGISTER_NOTES.md §11). The markup is the app's |
| 381 | // now. `SEAMED` is what a complying model sends; `SEAMBARE` is the same line with |
| 382 | // nothing in front of it, which must NOT fold, or the app has reproduced the one |
| 383 | // verdict CONTRACT_FOLD.md §5 calls worse than no control at all. |
| 384 | const SEAMED = 'The cache was cold, and it is warm again now.\n\n' |
| 385 | + 'Fold: The deploy dropped the warmed image and the replacement came up empty, so the ' |
| 386 | + 'first request after a cutover pays for the fill.\n\n' + BODY + '\n'; |
| 387 | const SEAMBARE = 'Fold: The deploy dropped the warmed image and the replacement came up ' |
| 388 | + 'empty, so the first request pays.\n\n' + BODY + '\n'; |
| 389 | |
| 390 | // A fold inside a fold. The renderer nests correctly for free, because the |
| 391 | // browser's parser does; what has to be proved is that the KEY does not, since a |
| 392 | // nested fold takes no ordinal (CONTRACT_FOLD.md §8) and an extra one here would |
| 393 | // rename every fold after it in the engine's eyes. |
| 394 | const NESTED = 'Here it is.\n\n<details>\n<summary>Outer label</summary>\n\n' |
| 395 | + 'The part above.\n\n<details>\n<summary>Inner label</summary>\n\n' |
| 396 | + 'The deepest part.\n\n</details>\n\nThe part below.\n\n</details>\n'; |
| 397 | |
| 398 | const s = await open({ name: 'twodepth', profile: PROFILE, route: serveBreaks }); |
| 399 | const page = s.page; |
| 400 | |
| 401 | /// The bubble a turn just drew, by the label of the fold in it. |
| 402 | const bubbleFor = (label) => page.evaluateHandle((lbl) => { |
| 403 | const all = [...document.querySelectorAll('#chat-output .chat-msg-assistant')]; |
| 404 | return all.reverse().find(b => { |
| 405 | const sm = b.querySelector('details > summary'); |
| 406 | return sm && sm.textContent.trim() === lbl; |
| 407 | }) || null; |
| 408 | }, label); |
| 409 | |
| 410 | try { |
| 411 | await page.waitForFunction(() => window.DaimondRender && window.DaimondRender.foldScan, |
| 412 | null, { timeout: 20000 }); |
| 413 | |
| 414 | // ── 0c. EVERY turn pushes the open set, not just the one that was checked ── |
| 415 | // |
| 416 | // Static, and deliberately so: the browser checks below drive an ordinary |
| 417 | // chat, and an ordinary chat was never the path that was broken. `doSteer` |
| 418 | // -- the Diamond's own thread, which is the surface Daimond is developed |
| 419 | // from -- set `_generating` and streamed without ever calling |
| 420 | // `pushOpenFolds`, so a Diamond's app carried an EMPTY open set for its |
| 421 | // whole life and every fold body was stripped from every payload. The |
| 422 | // feature worked, was tested, and was inert where it mattered most. |
| 423 | // |
| 424 | // So the property is not "a turn pushes the open set" but "there is no turn |
| 425 | // that does not". Each `_generating = true` is a turn beginning; the push |
| 426 | // must be within reach of it in the same function. A third turn path added |
| 427 | // without the call reddens here rather than in a month of use. |
| 428 | { |
| 429 | const js = fs.readFileSync(path.join(WWW, 'js/daimond.js'), 'utf8'); |
| 430 | const lines = js.split('\n'); |
| 431 | const starts = [], missing = []; |
| 432 | lines.forEach((ln, i) => { if (/_generating\s*=\s*true/.test(ln)) starts.push(i); }); |
| 433 | for (const i of starts) { |
| 434 | // The push sits beside the app the turn will run on, which is within a |
| 435 | // few lines of the flag in both existing paths. Sixty lines is wide |
| 436 | // enough for a commented one and far narrower than a function. |
| 437 | const near = lines.slice(Math.max(0, i - 60), i + 60).join('\n'); |
| 438 | if (!/pushOpenFolds\s*\(/.test(near)) missing.push(`daimond.js:${i + 1}`); |
| 439 | } |
| 440 | check(`0c every turn pushes what is open on screen (${starts.length} turn path(s))`, |
| 441 | starts.length >= 2 && missing.length === 0, |
| 442 | missing.length ? `no pushOpenFolds near ${missing.join(', ')}` |
| 443 | : `${starts.length} turn path(s), all push`); |
| 444 | } |
| 445 | |
| 446 | // ── 1. The key, against the fixture ─────────────────────────────── |
| 447 | // |
| 448 | // Body length is counted in CODE POINTS on both sides, because the contract |
| 449 | // says `chars().count()` and a JS `.length` counts UTF-16 units — the two part |
| 450 | // company on the first emoji a model puts in a fold. |
| 451 | const keyed = await page.evaluate((cases) => cases.map((c) => { |
| 452 | const folds = window.DaimondRender.foldScan(c.input); |
| 453 | return { keys: folds.map(f => f.key), chars: folds.map(f => [...f.body].length) }; |
| 454 | }), fixture.cases); |
| 455 | check('0 the fixture on disk was actually read', |
| 456 | Array.isArray(fixture.cases) && fixture.cases.length > 0 |
| 457 | && keyed.length === fixture.cases.length, |
| 458 | `${fixture.cases.length} case(s) from ${path.relative(path.join(HERE, '..'), FIXTURE)}`); |
| 459 | const wrong = []; |
| 460 | fixture.cases.forEach((c, i) => { |
| 461 | const okK = JSON.stringify(keyed[i].keys) === JSON.stringify(c.keys); |
| 462 | const okC = JSON.stringify(keyed[i].chars) === JSON.stringify(c.body_chars); |
| 463 | if (!okK || !okC) { |
| 464 | wrong.push(`${c.name}: keys ${JSON.stringify(keyed[i].keys)} want ${JSON.stringify(c.keys)}` |
| 465 | + `, chars ${JSON.stringify(keyed[i].chars)} want ${JSON.stringify(c.body_chars)}`); |
| 466 | } |
| 467 | }); |
| 468 | check(`1a every fixture case keys as the contract says (${fixture.cases.length})`, |
| 469 | wrong.length === 0, wrong.slice(0, 2).join(' | ')); |
| 470 | |
| 471 | // WHY THE RENDERER HOLDS A HALF-WRITTEN FOLD BACK, measured rather than |
| 472 | // assumed. `marked` hands raw HTML through, and the parser auto-closes an |
| 473 | // unfinished element at the end of the fragment -- so a `<details>` whose |
| 474 | // `</summary>` has not arrived is already a real, closed disclosure with a |
| 475 | // growing label. Drawn, it appears shut and then snaps open when the control |
| 476 | // proper replaces it. Recorded here because it is the reason for `foldPending` |
| 477 | // and nothing else in the tree says it. |
| 478 | const halfway = await page.evaluate(() => { |
| 479 | const d = document.createElement('div'); |
| 480 | d.innerHTML = window.DaimondRender.md('<details>\n<summary>Half typed'); |
| 481 | const f = d.querySelector('details'); |
| 482 | return f ? { drawn: true, label: (f.querySelector('summary') || {}).textContent } : { drawn: false }; |
| 483 | }); |
| 484 | check('1b an unfinished `<details>` really is auto-closed into a live control', |
| 485 | halfway.drawn === true && halfway.label === 'Half typed', JSON.stringify(halfway)); |
| 486 | |
| 487 | // THE HOLD-BACK ITSELF, asked deterministically. Check 3a below puts the same |
| 488 | // question to a real stream, but whether a frame lands inside the few |
| 489 | // milliseconds between `<details>` and `</summary>` is a race, and a prover |
| 490 | // that only sometimes reddens is not a prover. |
| 491 | const pending = await page.evaluate(() => { |
| 492 | const half = 'Above the fold.\n\n<details open>\n<summary>Half typ'; |
| 493 | const seg = (a) => a.map(x => x.kind + ':' + (x.kind === 'fold' ? x.key : x.text)); |
| 494 | return { |
| 495 | live: seg(window.DaimondRender.foldSegments(half, false)), |
| 496 | done: seg(window.DaimondRender.foldSegments(half, true)), |
| 497 | }; |
| 498 | }); |
| 499 | check('1c a half-written fold draws nothing at all until it IS a fold', |
| 500 | pending.live.length === 1 && pending.live[0] === 'text:Above the fold.\n\n', |
| 501 | JSON.stringify(pending.live)); |
| 502 | check('1d but a turn that died half way through one still shows what arrived', |
| 503 | pending.done.length === 1 && /<details open>/.test(pending.done[0]), |
| 504 | JSON.stringify(pending.done)); |
| 505 | |
| 506 | // ── The instrument ──────────────────────────────────────────────── |
| 507 | // |
| 508 | // Installed before the turn, sampling once per frame. A MutationObserver |
| 509 | // counts insertions rather than reading a count at the end, because "one fold |
| 510 | // on screen" is exactly what a fold rebuilt sixty times still looks like. |
| 511 | // |
| 512 | // SCOPED TO ONE BUBBLE, by the count of them standing before the turn began. |
| 513 | // "The last assistant message" is not the same question: the app may draw |
| 514 | // another one while this turn is still going, and a count of folds taken over |
| 515 | // the whole thread would then answer for two messages at once. |
| 516 | await page.evaluate(() => { |
| 517 | const out = document.getElementById('chat-output'); |
| 518 | const asst = () => out.querySelectorAll('.chat-msg-assistant'); |
| 519 | const st = { made: 0, uid: 0, samples: [], sent: [], base: asst().length }; |
| 520 | window.__twodepth = st; |
| 521 | const mine = () => asst()[st.base] || null; |
| 522 | const stamp = (el) => { |
| 523 | const b = mine(); |
| 524 | if (!b || !b.contains(el) || el.dataset.probeUid) return; |
| 525 | el.dataset.probeUid = String(++st.uid); |
| 526 | st.made++; |
| 527 | }; |
| 528 | const obs = new MutationObserver((recs) => { |
| 529 | for (const r of recs) { |
| 530 | for (const n of r.addedNodes) { |
| 531 | if (n.nodeType !== 1) continue; |
| 532 | if (n.matches && n.matches('details.md-fold')) stamp(n); |
| 533 | if (n.querySelectorAll) n.querySelectorAll('details.md-fold').forEach(stamp); |
| 534 | } |
| 535 | } |
| 536 | }); |
| 537 | obs.observe(out, { childList: true, subtree: true }); |
| 538 | const tick = () => { |
| 539 | const b = mine(); |
| 540 | if (b) { |
| 541 | const fold = b.querySelector('details.md-fold'); |
| 542 | const seg = b.querySelector('.md-seg'); |
| 543 | const body = b.querySelector('.md-fold-body'); |
| 544 | st.samples.push({ |
| 545 | all: b.innerText.length, |
| 546 | seg: seg ? seg.innerText.length : -1, |
| 547 | body: body ? body.innerText.length : -1, |
| 548 | fold: !!fold, |
| 549 | bubbles: asst().length, |
| 550 | }); |
| 551 | } |
| 552 | st.raf = requestAnimationFrame(tick); |
| 553 | }; |
| 554 | tick(); |
| 555 | st.stop = () => { obs.disconnect(); cancelAnimationFrame(st.raf); }; |
| 556 | }); |
| 557 | |
| 558 | // The engine's own door, wrapped and still called. What is recorded is what |
| 559 | // the wasm boundary was handed, not what this file hoped it would be. |
| 560 | const shimmed = await page.evaluate(async () => { |
| 561 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 562 | const P = m.DaimondApp && m.DaimondApp.prototype; |
| 563 | if (!P || typeof P.set_open_folds !== 'function') return false; |
| 564 | const orig = P.set_open_folds; |
| 565 | P.set_open_folds = function (json) { |
| 566 | window.__twodepth.sent.push(json); |
| 567 | return orig.call(this, json); |
| 568 | }; |
| 569 | return true; |
| 570 | }); |
| 571 | check('0b the engine offers `set_open_folds` to wrap', shimmed === true); |
| 572 | |
| 573 | // ── 2 & 3. A real turn, streamed ────────────────────────────────── |
| 574 | await chat(s, '@text ' + STREAMED); |
| 575 | const run = await page.evaluate(() => { |
| 576 | const st = window.__twodepth; |
| 577 | st.stop(); |
| 578 | return { made: st.made, samples: st.samples, sent: st.sent.slice() }; |
| 579 | }); |
| 580 | const seen = run.samples; |
| 581 | const last = seen[seen.length - 1] || { all: 0, seg: -1, body: -1, fold: false }; |
| 582 | const distinct = (k) => new Set(seen.map(x => x[k]).filter(v => v >= 0)).size; |
| 583 | const iFold = seen.findIndex(x => x.fold); |
| 584 | |
| 585 | check('2a the answer arrived a piece at a time, not all at the end', |
| 586 | distinct('all') >= 4, `${distinct('all')} distinct length(s) over ${seen.length} frame(s)`); |
| 587 | check('2b the text ABOVE the fold was complete on screen while the fold was still filling', |
| 588 | iFold >= 0 && seen[iFold].seg > 0 && seen[iFold].body < last.body |
| 589 | && seen[iFold].seg === last.seg, |
| 590 | iFold < 0 ? 'no fold was ever drawn' |
| 591 | : `at the fold's first frame: above=${seen[iFold].seg}/${last.seg}, ` |
| 592 | + `inside=${seen[iFold].body}/${last.body}`); |
| 593 | check('2c the fold\'s own body grew across frames', |
| 594 | distinct('body') >= 3 && seen.filter(x => x.body >= 0) |
| 595 | .every((x, i, a) => i === 0 || x.body >= a[i - 1].body), |
| 596 | `${distinct('body')} distinct length(s), final ${last.body}`); |
| 597 | check('3a the fold control was inserted ONCE for the whole stream', |
| 598 | run.made === 1, `${run.made} insertion(s) over ${seen.length} frame(s)`); |
| 599 | const uid = await page.evaluate(() => { |
| 600 | const b = document.querySelectorAll('#chat-output .chat-msg-assistant')[window.__twodepth.base]; |
| 601 | const d = b && b.querySelector('details.md-fold'); |
| 602 | return d ? d.dataset.probeUid : null; |
| 603 | }); |
| 604 | check('3b and the fold still on screen is that same element', |
| 605 | uid === '1', String(uid) + `; ${new Set(seen.map(x => x.bubbles)).size} bubble count(s) seen`); |
| 606 | |
| 607 | // ── 4. A closed fold's body ─────────────────────────────────────── |
| 608 | await chat(s, '@text ' + SHUT); |
| 609 | /// Shown, in two geometric facts at once. See the header: a closed fold's |
| 610 | /// contents keep their last layout, so a rect with area proves nothing alone. |
| 611 | const measure = (label) => page.evaluate((lbl) => { |
| 612 | const all = [...document.querySelectorAll('#chat-output details.md-fold')]; |
| 613 | const d = all.reverse().find(x => { |
| 614 | const sm = x.querySelector('summary'); |
| 615 | return sm && sm.textContent.trim() === lbl; |
| 616 | }); |
| 617 | if (!d) return null; |
| 618 | d.scrollIntoView({ block: 'center' }); |
| 619 | const el = d.querySelector('.md-fold-body > *'); |
| 620 | if (!el) return { open: d.open, rect: false, hit: false, why: 'no body' }; |
| 621 | const a = d.getBoundingClientRect(), c = el.getBoundingClientRect(); |
| 622 | const at = document.elementFromPoint(c.left + c.width / 2, c.top + c.height / 2); |
| 623 | return { |
| 624 | open: d.open, |
| 625 | // Real area, and inside the fold that is meant to be containing it. |
| 626 | rect: c.height > 0 && c.top >= a.top - 0.5 && c.bottom <= a.bottom + 0.5, |
| 627 | hit: !!at && d.contains(at), |
| 628 | h: Math.round(c.height), |
| 629 | }; |
| 630 | }, label); |
| 631 | const shut = await measure('What was actually wrong'); |
| 632 | check('4a a fold the model did not open starts closed', |
| 633 | !!shut && shut.open === false, shut ? JSON.stringify(shut) : 'no fold'); |
| 634 | check('4b and its body is not on screen — containment AND a hit test', |
| 635 | !!shut && !shut.rect && !shut.hit, shut ? JSON.stringify(shut) : null); |
| 636 | // `el.click()`, not page.click with force: a forced click does nothing at all |
| 637 | // on this app headless, and has flaked several older verifiers. |
| 638 | await page.evaluate((lbl) => { |
| 639 | const all = [...document.querySelectorAll('#chat-output details.md-fold')]; |
| 640 | const d = all.reverse().find(x => { |
| 641 | const sm = x.querySelector('summary'); |
| 642 | return sm && sm.textContent.trim() === lbl; |
| 643 | }); |
| 644 | if (d) d.querySelector('summary').click(); |
| 645 | }, 'What was actually wrong'); |
| 646 | await page.waitForTimeout(250); |
| 647 | const shown = await measure('What was actually wrong'); |
| 648 | check('4c pressing the label puts it on screen — the same two facts, the other way', |
| 649 | !!shown && shown.open === true && shown.rect && shown.hit, |
| 650 | shown ? JSON.stringify(shown) : null); |
| 651 | |
| 652 | // ── 5. The open set, at the wasm boundary ───────────────────────── |
| 653 | const sent = await page.evaluate(() => window.__twodepth.sent.slice()); |
| 654 | const lastSent = sent.length ? sent[sent.length - 1] : ''; |
| 655 | let parsed = null; |
| 656 | try { parsed = JSON.parse(lastSent); } catch (e) { /* the check below says so */ } |
| 657 | check('5a opening a fold hands the engine a set with that fold\'s key in it', |
| 658 | Array.isArray(parsed) && parsed.indexOf('0:What was actually wrong') >= 0, |
| 659 | lastSent ? lastSent.slice(0, 160) : 'nothing was ever handed to the engine'); |
| 660 | |
| 661 | // ── 6. A fold with nothing above it ─────────────────────────────── |
| 662 | await chat(s, '@text ' + BARE); |
| 663 | const bare = await measure('Everything I have'); |
| 664 | const bareMark = await page.evaluate(() => { |
| 665 | const all = [...document.querySelectorAll('#chat-output details.md-fold')]; |
| 666 | const d = all.reverse().find(x => { |
| 667 | const sm = x.querySelector('summary'); |
| 668 | return sm && sm.textContent.trim() === 'Everything I have'; |
| 669 | }); |
| 670 | return d ? { marked: d.classList.contains('md-fold-bare'), |
| 671 | above: (d.previousElementSibling || {}).textContent || '' } : null; |
| 672 | }); |
| 673 | check('6a a fold with nothing above it is drawn OPEN, not shut', |
| 674 | !!bare && bare.open === true && bare.rect && bare.hit, |
| 675 | bare ? JSON.stringify(bare) : 'no fold'); |
| 676 | check('6b and the renderer says so, rather than it being a coincidence', |
| 677 | !!bareMark && bareMark.marked === true && !String(bareMark.above).trim(), |
| 678 | bareMark ? JSON.stringify(bareMark) : null); |
| 679 | |
| 680 | // ── 7. A fenced fold is a model showing markup ──────────────────── |
| 681 | await chat(s, '@text ' + FENCED); |
| 682 | const fenced = await page.evaluate(() => { |
| 683 | const all = [...document.querySelectorAll('#chat-output .chat-msg-assistant')]; |
| 684 | const b = all.reverse().find(x => /Write it like this/.test(x.innerText || '')); |
| 685 | if (!b) return null; |
| 686 | return { |
| 687 | folds: b.querySelectorAll('details').length, |
| 688 | literal: /<details>/.test(b.innerText || ''), |
| 689 | code: b.querySelectorAll('pre code').length, |
| 690 | }; |
| 691 | }); |
| 692 | check('7a a `<details>` inside a fence draws no control', |
| 693 | !!fenced && fenced.folds === 0, fenced ? JSON.stringify(fenced) : 'no bubble'); |
| 694 | check('7b and the reader still sees the markup that was being shown', |
| 695 | !!fenced && fenced.literal && fenced.code > 0, |
| 696 | fenced ? JSON.stringify(fenced) : null); |
| 697 | |
| 698 | // ── 8. Nothing executable rides in on the label ─────────────────── |
| 699 | await chat(s, '@text ' + NASTY); |
| 700 | // The image has to be given time to fail before "nothing ran" means anything. |
| 701 | await page.waitForTimeout(900); |
| 702 | const nasty = await page.evaluate(() => { |
| 703 | const all = [...document.querySelectorAll('#chat-output details.md-fold')]; |
| 704 | const d = all.reverse().find(x => /Mind/.test((x.querySelector('summary') || {}).textContent || '')); |
| 705 | if (!d) return null; |
| 706 | const sum = d.querySelector('summary'); |
| 707 | return { |
| 708 | pwned: !!window.__pwned, |
| 709 | scripts: sum.querySelectorAll('script').length, |
| 710 | onerror: sum.querySelectorAll('[onerror]').length, |
| 711 | hrefs: [...sum.querySelectorAll('a')].map(a => a.getAttribute('href') || ''), |
| 712 | label: String(sum.textContent || '').replace(/\s+/g, ' ').trim(), |
| 713 | key: d.dataset.foldKey || '', |
| 714 | }; |
| 715 | }); |
| 716 | check('8a the label carries no script, no handler and no javascript: url', |
| 717 | !!nasty && nasty.pwned === false && nasty.scripts === 0 && nasty.onerror === 0 |
| 718 | && nasty.hrefs.every(h => !/^javascript:/i.test(h)), |
| 719 | nasty ? JSON.stringify(nasty) : 'no fold'); |
| 720 | check('8b but the words the model wrote are still its label', |
| 721 | !!nasty && nasty.label === 'Mind the gap', |
| 722 | nasty ? JSON.stringify(nasty.label) : null); |
| 723 | // THE KEY AND THE LABEL LEGITIMATELY DIFFER HERE, and it is worth pinning |
| 724 | // rather than filing off. §2 says the key is the summary's TEXT CONTENT with |
| 725 | // its tags removed, and a `<script>`'s body is text content -- a DOM reading |
| 726 | // gives the same answer as the scan does. The renderer drops the element |
| 727 | // whole, as it must. So the fold is NAMED with words nobody sees, and both |
| 728 | // halves of the app agree on that name, which is the only thing the key is |
| 729 | // for. |
| 730 | check('8c and the key keeps what the sanitiser threw away, because it is not the DOM', |
| 731 | !!nasty && nasty.key === '0:Mind window.__pwned=1; the gap', |
| 732 | nasty ? JSON.stringify(nasty.key) : null); |
| 733 | |
| 734 | // ── 9. A fold inside a fold ─────────────────────────────────────── |
| 735 | await chat(s, '@text ' + NESTED); |
| 736 | const nested = await page.evaluate(() => { |
| 737 | const all = [...document.querySelectorAll('#chat-output details.md-fold')]; |
| 738 | const outer = all.reverse().find(d => |
| 739 | (d.querySelector('summary') || {}).textContent.trim() === 'Outer label'); |
| 740 | if (!outer) return null; |
| 741 | // Opened here, because a nested fold is only worth asking about once the |
| 742 | // fold holding it is on screen. |
| 743 | outer.querySelector('summary').click(); |
| 744 | const inner = outer.querySelector('details.md-fold'); |
| 745 | return { |
| 746 | outerKey: outer.dataset.foldKey || '', |
| 747 | innerKey: inner ? (inner.dataset.foldKey || '') : '(no inner fold)', |
| 748 | contained: !!inner && outer.contains(inner), |
| 749 | outerOpen: outer.open, innerOpen: inner ? inner.open : null, |
| 750 | }; |
| 751 | }); |
| 752 | check('9a the outer fold takes ordinal 0 and the inner takes no key at all', |
| 753 | !!nested && nested.outerKey === '0:Outer label' && nested.innerKey === '', |
| 754 | nested ? JSON.stringify(nested) : 'no fold'); |
| 755 | check('9b and the inner one really is inside it, still shut when the outer opens', |
| 756 | !!nested && nested.contained && nested.outerOpen === true && nested.innerOpen === false, |
| 757 | nested ? JSON.stringify(nested) : null); |
| 758 | |
| 759 | await chat(s, '@text ' + ALONE); |
| 760 | |
| 761 | // ── 10. The reader's choice survives a reload ───────────────────── |
| 762 | // |
| 763 | // The fold opened at 4c, after a real reload of the page and a real unlock — |
| 764 | // not a redraw. The key holds no message identity, so nothing about the |
| 765 | // message had to be stored for this to work. |
| 766 | await page.reload({ waitUntil: 'domcontentloaded' }); |
| 767 | await signInAs(s, 'twodepth'); |
| 768 | await page.waitForSelector('#chat-output details.md-fold', { timeout: 20000 }) |
| 769 | .catch(() => { /* the check below says what was there */ }); |
| 770 | await page.waitForTimeout(600); |
| 771 | const after = await page.evaluate(() => { |
| 772 | const all = [...document.querySelectorAll('#chat-output details.md-fold')]; |
| 773 | return all.map(d => ({ |
| 774 | label: (d.querySelector('summary') || {}).textContent.trim(), |
| 775 | open: d.open, |
| 776 | })); |
| 777 | }); |
| 778 | const restored = after.find(x => x.label === 'What was actually wrong'); |
| 779 | const untouched = after.find(x => x.label === 'Left alone'); |
| 780 | check('10a the fold the reader opened comes back open', |
| 781 | !!restored && restored.open === true, JSON.stringify(after)); |
| 782 | check('10b and one the reader never touched comes back shut', |
| 783 | !!untouched && untouched.open === false, JSON.stringify(after)); |
| 784 | |
| 785 | // ── 12. Narration is working, and the last word is the answer ───── |
| 786 | // |
| 787 | // `@narrate` makes the mock send prose AND a tool call in one message, which no fixture |
| 788 | // here could do until today -- and a provider does it constantly. Both halves are checked |
| 789 | // in one turn, because the rule is a DIVISION and testing one side of it would pass on a |
| 790 | // build that folded everything, reply included. |
| 791 | await chat(s, '@narrate Let me pin the exact line numbers first, and check nothing else ' |
| 792 | + 'renders that key. ;; file_list {"path":"."}'); |
| 793 | await page.waitForTimeout(900); |
| 794 | const split = await page.evaluate(() => { |
| 795 | const out = document.querySelector('#chat-output'); |
| 796 | const working = [...out.querySelectorAll('.chat-msg-working')]; |
| 797 | const answers = [...out.querySelectorAll('.chat-msg-assistant')] |
| 798 | .map(d => (d.textContent || '').trim()).filter(Boolean); |
| 799 | return { |
| 800 | working: working.length, |
| 801 | workingSaid: working.map(w => (w.textContent || '').trim().slice(0, 60)), |
| 802 | // The narration must NOT be in any assistant bubble. |
| 803 | leaked: answers.some(t => t.indexOf('Let me pin the exact line numbers') >= 0), |
| 804 | // And the turn's last word must still be a real answer. |
| 805 | answered: answers.some(t => t.indexOf('Narration done') >= 0), |
| 806 | }; |
| 807 | }); |
| 808 | check('12a prose that precedes a tool call is drawn as working', |
| 809 | !!split && split.working >= 1, JSON.stringify(split)); |
| 810 | check('12b and is NOT left in the thread as an answer', |
| 811 | !!split && split.leaked === false, JSON.stringify(split)); |
| 812 | check('12c while the reply after the last call is still an answer', |
| 813 | !!split && split.answered === true, JSON.stringify(split)); |
| 814 | // AND IT DOES NOT HIDE WITH THE TOOL STEPS, WHICH IS A REVERSAL. |
| 815 | // |
| 816 | // This check read `hidden === false` from the day the working tile was written until |
| 817 | // 2026-08-28, on the 2026-08-23 reasoning that a reader who turned the working off meant |
| 818 | // this too. What the rule actually did was take the MODEL'S OWN SENTENCES off the screen: |
| 819 | // a turn of twenty tool calls drew one final answer and nothing else, which is note 05 of |
| 820 | // the 2026-08-27 round arriving by its other door. The owner reversed it. The switch is |
| 821 | // called Steps and it hides the steps; the working is collapsed, not hidden. |
| 822 | // |
| 823 | // The tool step is measured in the same breath, because the reversal must not have taken |
| 824 | // the switch's real job with it. `dev/verify_visible.mjs` measures both as ink on a real |
| 825 | // screen; this pins the rule. |
| 826 | const underSwitch = await page.evaluate(() => { |
| 827 | const out = document.querySelector('#chat-output'); |
| 828 | out.classList.add('hide-tools'); |
| 829 | const w = out.querySelector('.chat-msg-working'); |
| 830 | const t = out.querySelector('.tool-block'); |
| 831 | const seen = { |
| 832 | working: w ? getComputedStyle(w).display !== 'none' : null, |
| 833 | step: t ? getComputedStyle(t).display !== 'none' : null, |
| 834 | }; |
| 835 | out.classList.remove('hide-tools'); |
| 836 | return seen; |
| 837 | }); |
| 838 | check('12d the working does NOT hide with the tool steps — it is the model\'s own prose', |
| 839 | underSwitch.working === true, JSON.stringify(underSwitch)); |
| 840 | check('12e while the tool step still does, which is what the switch is for', |
| 841 | underSwitch.step === false, JSON.stringify(underSwitch)); |
| 842 | |
| 843 | // ── 13. The seam the APP places ─────────────────────────────────── |
| 844 | // |
| 845 | // Everything above this point tests a fold the MODEL wrote, and 5 answers in 76 |
| 846 | // carried one. The app places the seam now: the model writes a line beginning |
| 847 | // `Fold:` and the app builds the element, keys it, strips it and draws it. So |
| 848 | // what has to be proved is the expansion -- character for character, because |
| 849 | // the engine builds the same string in Rust and a key the two disagree about is |
| 850 | // a fold the reader opens that never leaves the payload -- and the refusals, |
| 851 | // which are the half that keeps a length rule from becoming FOLD-ALL. |
| 852 | const seamed = await page.evaluate((cases) => cases.map((c) => { |
| 853 | const got = window.DaimondRender.seamText(c.input, true); |
| 854 | return { out: got === c.input ? null : got, |
| 855 | keys: window.DaimondRender.foldScan(got).map(f => f.key) }; |
| 856 | }), seamfix.cases); |
| 857 | check('13 the seam fixture on disk was actually read', |
| 858 | Array.isArray(seamfix.cases) && seamfix.cases.length >= 14 |
| 859 | && seamed.length === seamfix.cases.length, |
| 860 | `${seamfix.cases.length} case(s) from ${path.relative(path.join(HERE, '..'), SEAMFIX)}`); |
| 861 | const seamwrong = []; |
| 862 | seamfix.cases.forEach((c, i) => { |
| 863 | const want = c.seamed === null || c.seamed === undefined ? null : c.seamed; |
| 864 | if (seamed[i].out !== want) { |
| 865 | seamwrong.push(`${c.name}: got ${JSON.stringify(String(seamed[i].out).slice(0, 90))}`); |
| 866 | } else if (JSON.stringify(seamed[i].keys) !== JSON.stringify(c.keys)) { |
| 867 | seamwrong.push(`${c.name}: keys ${JSON.stringify(seamed[i].keys)}`); |
| 868 | } |
| 869 | }); |
| 870 | check(`13a every seam case expands as the contract says (${seamfix.cases.length})`, |
| 871 | seamwrong.length === 0, seamwrong.slice(0, 2).join(' | ')); |
| 872 | |
| 873 | // A LINE THAT WOULD FOLD EVERYTHING FOLDS NOTHING, on screen and not in a unit |
| 874 | // test: the words the model wrote are all still there, and there is no control |
| 875 | // over them. This is the check the whole design turns on. |
| 876 | await chat(s, '@text ' + SEAMBARE); |
| 877 | const headless = await page.evaluate(() => { |
| 878 | const b = [...document.querySelectorAll('#chat-output .chat-msg-assistant')].pop(); |
| 879 | if (!b) return { why: 'no bubble' }; |
| 880 | return { |
| 881 | folds: b.querySelectorAll('details.md-fold').length, |
| 882 | marker: /(^|\n)\s*Fold:/.test(b.innerText), |
| 883 | kept: b.innerText.indexOf('the replacement came up empty') >= 0, |
| 884 | shown: b.innerText.length, |
| 885 | }; |
| 886 | }); |
| 887 | check('13b a seam with nothing above it folds NOTHING, and keeps every word', |
| 888 | headless.folds === 0 && headless.kept === true && headless.marker === false, |
| 889 | JSON.stringify(headless)); |
| 890 | |
| 891 | // AND THE QUALIFYING ONE FOLDS, or 13b would pass on a build where the seam |
| 892 | // never fired at all. |
| 893 | await chat(s, '@text ' + SEAMED); |
| 894 | const made = await page.evaluate(() => { |
| 895 | const b = [...document.querySelectorAll('#chat-output .chat-msg-assistant')].pop(); |
| 896 | const d = b && b.querySelector('details.md-fold'); |
| 897 | if (!d) return { why: 'no fold', text: b ? b.innerText.slice(0, 80) : null }; |
| 898 | const sum = d.querySelector('summary'); |
| 899 | const above = [...b.querySelectorAll('p')].find(x => !d.contains(x)); |
| 900 | return { |
| 901 | key: d.dataset.foldKey, |
| 902 | open: d.open, |
| 903 | label: sum ? sum.textContent.trim() : null, |
| 904 | above: above ? above.textContent.trim() : null, |
| 905 | // The body is inside the control and not beside it: a seam that cut in |
| 906 | // the wrong place would leave the working in the bubble either way. |
| 907 | inside: d.textContent.indexOf('the warmer runs after the cutover') >= 0, |
| 908 | beside: b.innerText.indexOf('the warmer runs after the cutover') >= 0, |
| 909 | marker: /(^|\n)\s*Fold:/.test(b.innerText), |
| 910 | }; |
| 911 | }); |
| 912 | check('13c a qualifying seam becomes one real fold, keyed as the contract says', |
| 913 | made.key === '0:' + made.label && /deploy dropped the warmed image/.test(made.label || ''), |
| 914 | JSON.stringify(made).slice(0, 200)); |
| 915 | check('13d with the answer above it and the working inside it, shut', |
| 916 | made.above === 'The cache was cold, and it is warm again now.' |
| 917 | && made.inside === true && made.beside === false && made.marker === false, |
| 918 | JSON.stringify(made).slice(0, 200)); |
| 919 | |
| 920 | // MID-STREAM, the one outcome the engine cannot have: a seam whose body has not |
| 921 | // arrived yet is held back rather than drawn and then unwound. `foldPending` |
| 922 | // makes the same argument for a half-written `<details>` and 1c proves it there. |
| 923 | const holding = await page.evaluate(() => { |
| 924 | const half = 'The cache was cold, and it is warm again now.\n\n' |
| 925 | + 'Fold: The deploy dropped the warmed image and the replacement came up empty.\n\nSo f'; |
| 926 | const seg = (a) => a.map(x => x.kind + ':' + (x.kind === 'fold' ? x.key : x.text)); |
| 927 | return { live: seg(window.DaimondRender.foldSegments(half, false)), |
| 928 | done: seg(window.DaimondRender.foldSegments(half, true)) }; |
| 929 | }); |
| 930 | check('13e a seam whose body is still arriving shows neither a fold nor the marker', |
| 931 | holding.live.length === 1 |
| 932 | && holding.live[0] === 'text:The cache was cold, and it is warm again now.\n\n', |
| 933 | JSON.stringify(holding.live).slice(0, 160)); |
| 934 | check('13f and a turn that died before the body arrived still shows what did', |
| 935 | holding.done.length === 1 && /Fold:/.test(holding.done[0]) === false |
| 936 | && /came up empty/.test(holding.done[0]), |
| 937 | JSON.stringify(holding.done).slice(0, 160)); |
| 938 | |
| 939 | // ── 11. The label is legible ────────────────────────────────────── |
| 940 | // |
| 941 | // A fold nobody finds is a fold nobody opens, and every check above this one |
| 942 | // passes on exactly that. The owner, 2026-08-23, located a real fold on his own |
| 943 | // screen only after being told it was there: the summary was `--text-muted` at |
| 944 | // `--fs-sm`, which between a reply above and a reply below reads as a caption |
| 945 | // for something rather than as a thing to read. |
| 946 | // |
| 947 | // MEASURED AGAINST THE ANSWER IT BELONGS TO, never against a value written |
| 948 | // here. A check pinned to `#ECE6DC` would go red on a theme and green on a |
| 949 | // regression under the pink one; a check pinned to the answer's own computed |
| 950 | // style moves with every theme and still catches a summary set apart as minor. |
| 951 | const legible = await page.evaluate(() => { |
| 952 | const d = [...document.querySelectorAll('#chat-output details.md-fold')] |
| 953 | .filter(x => !x.classList.contains('chat-msg-thinking')).pop(); |
| 954 | if (!d) return { why: 'no fold on screen' }; |
| 955 | const sum = d.querySelector('summary'); |
| 956 | const bubble = d.closest('.chat-msg-content') || d.parentElement; |
| 957 | // The answer above the fold, in the same bubble. `!d.contains` because the |
| 958 | // fold's own body is full of paragraphs and one of those proves nothing. |
| 959 | const above = [...bubble.querySelectorAll('p')].find(x => !d.contains(x)); |
| 960 | if (!sum || !above) return { why: 'no summary, or nothing above the fold' }; |
| 961 | const a = getComputedStyle(sum), b = getComputedStyle(above); |
| 962 | return { size: a.fontSize, answerSize: b.fontSize, |
| 963 | ink: a.color, answerInk: b.color }; |
| 964 | }); |
| 965 | check('11a the summary is set at the size of the answer it belongs to', |
| 966 | !!legible && legible.size === legible.answerSize, JSON.stringify(legible)); |
| 967 | check('11b and in the same ink, not the grey of a caption', |
| 968 | !!legible && legible.ink === legible.answerInk, JSON.stringify(legible)); |
| 969 | |
| 970 | // The attack's own 404 is expected -- the image is MEANT to fail, which is what |
| 971 | // makes 8a mean anything -- so it is not counted as the app throwing. |
| 972 | const errs = errors(s).filter(e => !/Failed to load resource/.test(e)) |
| 973 | .filter(e => !/no-such-image-7731/.test(e)); |
| 974 | check('nothing threw while it was on screen', errs.length === 0, errs.slice(0, 3).join(' | ')); |
| 975 | |
| 976 | // The picture, with the ink centred: the guide put away so the thread has the |
| 977 | // width, and the fold that streamed opened in the middle of it. A fold that is |
| 978 | // technically right and visually wrong is not done, and the only way to know |
| 979 | // which it is, is to look. |
| 980 | await page.evaluate(() => { |
| 981 | // The guide rides in the Web panel; its own closer sends the panel back to |
| 982 | // the tag row, which is what a reader would do before settling in to read. |
| 983 | const x = document.querySelector('#panel-web [data-close="web"]'); |
| 984 | if (x) x.click(); |
| 985 | }); |
| 986 | await page.waitForTimeout(500); |
| 987 | await page.evaluate(() => { |
| 988 | const d = [...document.querySelectorAll('#chat-output details.md-fold')] |
| 989 | .find(x => (x.querySelector('summary') || {}).textContent.trim() === 'The long version'); |
| 990 | if (d) { d.open = true; d.scrollIntoView({ block: 'center' }); } |
| 991 | }); |
| 992 | await page.waitForTimeout(400); |
| 993 | await shot(s, 'twodepth' + (BREAK ? '-' + BREAK : '')); |
| 994 | // THE SEAM ON SCREEN, both ways round, because four claims about this product |
| 995 | // have been made from source alone and every one was wrong. One picture of an |
| 996 | // answer the app folded and one of an answer it refused to fold. |
| 997 | await page.evaluate(() => { |
| 998 | const d = [...document.querySelectorAll('#chat-output details.md-fold')] |
| 999 | .find(x => /deploy dropped the warmed image/.test( |
| 1000 | (x.querySelector('summary') || {}).textContent || '')); |
| 1001 | if (d) { d.open = false; d.scrollIntoView({ block: 'center' }); } |
| 1002 | }); |
| 1003 | await page.waitForTimeout(400); |
| 1004 | await shot(s, 'twodepth-seam-folded' + (BREAK ? '-' + BREAK : '')); |
| 1005 | await page.evaluate(() => { |
| 1006 | const b = [...document.querySelectorAll('#chat-output .chat-msg-assistant')] |
| 1007 | .filter(x => x.querySelectorAll('details.md-fold').length === 0).pop(); |
| 1008 | if (b) b.scrollIntoView({ block: 'center' }); |
| 1009 | }); |
| 1010 | await page.waitForTimeout(400); |
| 1011 | await shot(s, 'twodepth-seam-refused' + (BREAK ? '-' + BREAK : '')); |
| 1012 | } catch (e) { |
| 1013 | check('the run got to the end of itself', false, |
| 1014 | String(e && e.message ? e.message : e).split('\n')[0]); |
| 1015 | try { await shot(s, 'twodepth-threw' + (BREAK ? '-' + BREAK : '')); } catch (e2) { /* none */ } |
| 1016 | } finally { |
| 1017 | await s.close(); |
| 1018 | } |
| 1019 | |
| 1020 | if (BREAK) { |
| 1021 | console.log(`\nbreak '${BREAK}': ${bad.length} check(s) failed` |
| 1022 | + (bad.length ? ' — ' + bad.join('; ') : ' — NOTHING FAILED, so the checks above prove nothing')); |
| 1023 | process.exit(bad.length ? 0 : 1); // a break MUST fail something |
| 1024 | } |
| 1025 | console.log(bad.length === 0 |
| 1026 | ? `\ntwodepth: all ${ok.length} checks passed` |
| 1027 | : `\ntwodepth: ${bad.length} of ${ok.length + bad.length} checks FAILED`); |
| 1028 | process.exit(bad.length === 0 ? 0 : 1); |