From 7a28dbb55ee53310b6bcad06f3728690662685d1 Mon Sep 17 00:00:00 2001 From: Mikkel Garcia Date: Tue, 28 Jul 2026 21:54:31 -0600 Subject: [PATCH 01/11] Guard the index.html <-> play.html twin: fail a one-sided edit index.html (editor) and play.html (app player + export) share 696 hand-maintained byte-identical lines longer than 40 characters. Every engine change is a 2-surface edit. The existing twin guards each pin one named function (resizePlan, the pricing resolver, MP4CAT, cost accrual). check-js-parity and check-njs-*-delegation pin each surface against the sibling nanoodle-js bundle. Nothing compared the two files as wholes, so a one-sided edit to any other shared line shipped silently. scripts/check-twin-drift.mjs measures the shared set and pins it against scripts/twin-drift-baseline.json: - A line that leaves the set because 1 surface moved and the other did not FAILS, with both file:line positions and the 2 line versions. - The count is a ratchet. Extraction lowers it freely. New duplication cannot raise it silently. - Generated regions are blanked first (the njs-engine bundle, the probed PROMPT-CAPS table, the i18n maps, the Runware AIR table), so the library is never booked as hand duplication. - A correctly mirrored edit passes. Membership moves, the count holds, and the guard asks for a refresh in a note. Refresh: TWIN_DRIFT_UPDATE=1 node scripts/check-twin-drift.mjs docs/twin-drift.md ranks the 90 duplicated blocks and gives a verdict for each: covered by the bundle path, extractable, or surface-specific. Co-Authored-By: Claude Opus 5 (1M context) --- .githooks/pre-commit | 19 + docs/twin-drift.md | 245 +++++++++++ scripts/check-twin-drift.mjs | 361 ++++++++++++++++ scripts/twin-drift-baseline.json | 705 +++++++++++++++++++++++++++++++ 4 files changed, 1330 insertions(+) create mode 100644 docs/twin-drift.md create mode 100644 scripts/check-twin-drift.mjs create mode 100644 scripts/twin-drift-baseline.json diff --git a/.githooks/pre-commit b/.githooks/pre-commit index f0f5c93..064dfcb 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -115,6 +115,11 @@ touches_drift=$(printf '%s\n' "$staged" | grep -E '(^|/)(index\.html|play\.html| # never sent to a 400 the user can't act on — and both engines must read the same generated table. touches_promptcaps=$(printf '%s\n' "$staged" | grep -E '(^|/)(index\.html|play\.html|scripts/check-prompt-caps\.mjs)$' || true) +# index.html + play.html share ~700 hand-maintained byte-identical lines; the targeted twin checks +# (resize/pricing/combine/cost) only pin named functions. This one pins the WHOLE overlap: a one-sided +# edit to any shared line fails, and new duplication cannot enter silently. +touches_twindrift=$(printf '%s\n' "$staged" | grep -E '(^|/)(index\.html|play\.html|scripts/check-twin-drift\.mjs|scripts/twin-drift-baseline\.json)$' || true) + # an imported (#a=) app's spend-capable bridge calls must be refused until a real Run gesture arms it (consent gate). touches_consent=$(printf '%s\n' "$staged" | grep -E '(^|/)(play\.html|scripts/check-bridge-consent\.mjs)$' || true) # play.html bakes creator sample results into #a= shares so keyless recipients see what the app makes; @@ -486,3 +491,17 @@ fi if [ -n "$touches_promptcaps" ]; then node "$root/scripts/check-prompt-caps.mjs" fi + +# 41) TWIN DRIFT: index.html (editor) and play.html (app player + export) carry ~700 hand-maintained +# byte-identical lines. The other twin checks each pin ONE named function; nothing compared the +# two files as wholes, so a one-sided edit anywhere else shipped silently. This guard pins the +# whole shared set against scripts/twin-drift-baseline.json: a line that leaves the set because +# one surface moved and the other did not FAILS with both file:line positions. The count is a +# ratchet — extraction may lower it freely, new duplication may not raise it silently. Generated +# regions (the njs-engine bundle, the probed prompt-cap table) are excluded, so the library is +# never booked as hand duplication. Offline, ~0.2s. Refresh deliberately: +# TWIN_DRIFT_UPDATE=1 node scripts/check-twin-drift.mjs +# Ranked extraction plan: docs/twin-drift.md +if [ -n "$touches_twindrift" ]; then + node "$root/scripts/check-twin-drift.mjs" +fi diff --git a/docs/twin-drift.md b/docs/twin-drift.md new file mode 100644 index 0000000..9d78558 --- /dev/null +++ b/docs/twin-drift.md @@ -0,0 +1,245 @@ +# Twin drift: the index.html ↔ play.html extraction map + +Date: 2026-07-28. Measured at commit `dbd4543`. + +## The measurement + +`index.html` is 12,493 lines. `play.html` is 13,639 lines. They are the two engine surfaces: + +- `index.html` — the editor. It may load files from `vendor/`. +- `play.html` — the app player and the single-file `.html` export. It must stay 1 self-contained file. + +**696 distinct lines longer than 40 characters appear byte-identically in both files.** They occur +749 times in `index.html` and 771 times in `play.html`. The generated ` in the file. It blanked play.html:11240-13637: 2,398 lines, 17.6% of the file, all of it hand-written app-player code. Renaming usd_balance at play.html:12698 (a byte-identical twin of the live balance parse at index.html:8101) passed the guard. So did every other one-sided edit to boot(), the share and export builder, the balance cache, the __appready__ handoff and the model picker. The guard now matches ` in the file. That match blanked `play.html:11240-13637`: 2,398 lines, 17.6% of +the file, all of it hand-written player code. 197 shared lines were invisible while it did. + +The guard therefore matches `` match. play.html +// also contains the STRING LITERAL that the export builder uses to re-emit the tag: +// const engTag = engText ? '` in the file. That blanked 2,398 +// lines of hand-written player code — boot(), the export builder, the balance cache, the +// __appready__ handoff, the model picker — and made a one-sided edit anywhere in them invisible. +// +// scripts/gen-js-engine.mjs:165 writes data-hash = sha256(bundle).slice(0,16). We re-derive it. A +// quoted string inside RUNTIME_JS cannot forge a body whose sha256 matches its own declared hash. +const NJS_BLOCK = /", + "64ec20d158e524c1 1 1 return { url: normalizeLoraUrl(r.url), scale: (r.strength==null||r.strength===\"\") ? 1 : Number(r.strength) };", + "6552cb2e39aa8a3d 1 1 const n = samples.length, dataLen = n*2, ab = new ArrayBuffer(44+dataLen), dv = new DataView(ab);", + "65d35cc6f5cd7704 17 13 /* ======================================================================", + "6630ac328cbea226 1 1 for(let i=0;i rej(new Error(\"couldn’t read that image to resize\"));", + "67c3dca541f159ed 1 1 if(f.reasoningEffort && f.reasoningEffort!==\"default\") o.reasoning_effort = f.reasoningEffort;", + "67ddf1729306bc1c 1 1 // buttons can rebuild them against a freshly-shortened link at click time (see below).", + "67f344afa5ef0f66 1 1 function audioUnitUsd(pricing, chars, seconds){", + "68e31cdca7ce4479 1 1 if(prompt) body.prompt = prompt; // omit when blank — upscalers run with no instruction; every other path guarantees a non-empty prompt before reaching here", + "696fa37f0d59a1c8 1 1 if(p.per_target_megapixel_second!=null){ const mp=_num(p.default_target_megapixels)||1; v=_num(p.per_target_megapixel_second)*mp*dur; return Math.max(v, _num(p.minimum_price)||0); }", + "6982f55283b9e63f 1 1 ", + "69f32ca62ef39f92 1 1 //