Packages

A real-browser external-app verifier for patch-safe Phoenix LiveView interactions, with secondary unstyled reference components.

Current section

Files

Jump to
live_interaction_contracts priv harness ledger.mjs
Raw

priv/harness/ledger.mjs

// Post-process results.json into the delegation ledger (JSON + Markdown).
// Classification is mechanical from the survival matrix; rationale strings are
// fixed per machine. Reads RESULTS_FILE in cwd; writes ../delegation-ledger.{json,md}.
import { readFileSync, writeFileSync } from 'node:fs';
const LEDGER_DIR = process.env.LEDGER_DIR || '..';
const RESULTS_FILE = process.env.RESULTS_FILE || 'results-1.2.5.json';
const r = JSON.parse(readFileSync(RESULTS_FILE));
const engines = Object.keys(r.engines);
const NONDESTRUCTIVE = ['siblingPatch', 'contentPatch', 'attributePatch', 'reorderNearby', 'streamNearby', 'pushPatch', 'reconnect'];
const survived = (m, after) => {
if (!after) return false;
switch (m) {
case 'pop': return after.open === true;
case 'dlg': case 'det': return after.open === true;
case 'sel': return after.value === 'b';
case 'inp': return after.value === 'typed-value' && after.focused === true;
case 'scr': return after.scrollTop === 500;
}
};
const META = {
pop: { name: 'popover', stateLocation: 'top-layer membership + :popover-open pseudo-class (NOT an attribute)' },
dlg: { name: 'dialog (modal)', stateLocation: 'server-reconciled `open` attribute' },
det: { name: 'details / summary', stateLocation: 'server-reconciled `open` attribute' },
sel: { name: 'native select (value/selection)', stateLocation: 'browser editing state (value property); native popup not automatable' },
inp: { name: 'focus / selection / input editing', stateLocation: 'DOM node-bound editing state + document focus' },
scr: { name: 'scroll position', stateLocation: 'DOM node-bound (scrollTop)' },
};
const RATIONALE = {
pop: 'green — openness lives in the top layer, not in any server-renderable attribute, so reconciliation cannot touch it; survives every non-destructive patch on every engine.',
dlg: 'red — modal openness is reflected into the `open` attribute; the server template omits it; reconciliation strips it on EVERY non-destructive patch, closing the dialog (node survives; state does not). Leaves an inconsistent state: open=false while :modal still matches.',
det: 'red — same mechanism as dialog: `open` is a reflected attribute stripped by reconciliation on every non-destructive patch. Confirms the failure is not dialog-specific but general to attribute-reflected browser state.',
sel: 'green (selection) / unknown (popup) — the selected value is browser editing state and survives every non-destructive patch; the open state of the native option popup is not observable via automation and is marked unknown.',
inp: 'green — value, selection range, and focus survive every non-destructive patch; a server write to the focused input value is ignored (documented focused-input preservation). Focus is DOM node-bound and lost only when node continuity breaks (remove/reinsert, push_navigate).',
scr: 'green — scrollTop is node-bound and not reflected into any attribute; survives every non-destructive patch.',
};
const classify = (machine, perEngine) => {
// green if every non-destructive patch survived on every engine
const allSurv = engines.every((e) => NONDESTRUCTIVE.every((p) => perEngine[e][p].survived));
const anyDestructiveSurv = engines.some((e) => ['pushNavigate'].some((p) => perEngine[e][p]?.survived));
if (machine === 'sel') return allSurv ? 'green (selection) / unknown (popup)' : 'amber';
if (allSurv) return 'green';
const noneSurv = engines.every((e) => NONDESTRUCTIVE.every((p) => !perEngine[e][p].survived));
return noneSurv ? 'red' : 'amber';
};
const ledger = { versions: r.versions, generatedFrom: RESULTS_FILE, machines: {}, notes: [] };
for (const [id, meta] of Object.entries(META)) {
const perEngine = {};
for (const e of engines) {
const patches = r.engines[e].runs[0][id].patches;
perEngine[e] = {};
for (const [pk, pv] of Object.entries(patches)) {
perEngine[e][pk] = { survived: survived(id, pv.after), nodeAlive: pv.nodeAlive };
}
}
// cross-browser difference?
const browserDiff = !engines.every((e) =>
JSON.stringify(Object.fromEntries(NONDESTRUCTIVE.map((p) => [p, perEngine[e][p].survived]))) ===
JSON.stringify(Object.fromEntries(NONDESTRUCTIVE.map((p) => [p, perEngine[engines[0]][p].survived]))));
ledger.machines[id] = {
name: meta.name,
class: classify(id, perEngine),
stateLocation: meta.stateLocation,
rationale: RATIONALE[id],
browserDifference: browserDiff,
survivalByPatch: Object.fromEntries(
[...NONDESTRUCTIVE, 'pushNavigate', 'removeReinsert'].map((p) => [
p,
{ survived: engines.every((e) => perEngine[e][p]?.survived), nodeAlive: perEngine[engines[0]][p]?.nodeAlive, destructiveControl: ['pushNavigate', 'removeReinsert'].includes(p) },
])
),
};
}
ledger.machines.ime = { name: 'IME composition', class: 'unknown', stateLocation: 'browser composition state (node-bound)', rationale: 'unknown — not reliably synthesizable via Playwright across engines; not tested.', browserDifference: null };
writeFileSync(LEDGER_DIR + "/delegation-ledger.json", JSON.stringify(ledger, null, 2));
// ---- markdown ----
const cell = (b) => (b === true ? '✓' : b === false ? '✗' : '?');
let md = `# Delegation Ledger\n\n`;
md += `Generated mechanically from \`harness/${RESULTS_FILE}\`. LiveView ${ledger.versions.live_view}, Phoenix ${ledger.versions.phoenix}, Elixir ${ledger.versions.elixir}, OTP ${ledger.versions.otp}. Engines: ${engines.map((e) => e + ' ' + r.engines[e].version).join(', ')}. Two runs per engine, byte-identical.\n\n`;
md += `**Class legend:** green = delegation survived all tested non-destructive patches · amber = works only with restrictions · red = fails under ordinary patching · unknown = insufficient evidence.\n\n`;
md += `## Classification\n\n| Machine | Class | State lives in | Cross-browser diff |\n|---|---|---|---|\n`;
for (const [id, m] of Object.entries(ledger.machines)) {
md += `| ${m.name} | **${m.class}** | ${m.stateLocation} | ${m.browserDifference === null ? 'n/a' : m.browserDifference ? 'YES' : 'no'} |\n`;
}
md += `\n## Survival matrix (✓ delegated state survived · ✗ died · destructive controls in italics)\n\n`;
const cols = [...NONDESTRUCTIVE, 'pushNavigate', 'removeReinsert'];
md += `| Machine | ${cols.map((c) => (['pushNavigate', 'removeReinsert'].includes(c) ? '*' + c + '*' : c)).join(' | ')} |\n`;
md += `|${'---|'.repeat(cols.length + 1)}\n`;
for (const [id, m] of Object.entries(ledger.machines)) {
if (!m.survivalByPatch) continue;
md += `| ${m.name} | ${cols.map((c) => cell(m.survivalByPatch[c]?.survived)).join(' | ')} |\n`;
}
md += `\n## Node-object survival (JS expando) — for the same matrix\n\n`;
md += `| Machine | ${cols.map((c) => (['pushNavigate', 'removeReinsert'].includes(c) ? '*' + c + '*' : c)).join(' | ')} |\n`;
md += `|${'---|'.repeat(cols.length + 1)}\n`;
for (const [id, m] of Object.entries(ledger.machines)) {
if (!m.survivalByPatch) continue;
md += `| ${m.name} | ${cols.map((c) => cell(m.survivalByPatch[c]?.nodeAlive)).join(' | ')} |\n`;
}
md += `\n## Rationale\n\n`;
for (const [id, m] of Object.entries(ledger.machines)) md += `- **${m.name}** — ${m.rationale}\n`;
md += `\n## Key cross-cut\n\nEvery **red** machine (dialog, details) stores its state in a *server-reconciled attribute* (\`open\`). Every **green** machine stores its state *off* server-reconciled attributes (top layer, editing state, scrollTop, focus). The node-object survival table is nearly all ✓ for non-destructive patches — proving node identity is **necessary but not sufficient**: dialog and details keep their node and still lose their state.\n`;
writeFileSync(LEDGER_DIR + "/delegation-ledger.md", md);
console.log('wrote delegation-ledger.json and delegation-ledger.md');