Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ touches_promptcaps=$(printf '%s\n' "$staged" | grep -E '(^|/)(index\.html|play\.
# growth/ is gitignored, so these paths only appear here when someone force-adds them — which is when it matters.
touches_launchfacts=$(printf '%s\n' "$staged" | grep -E '(^|/)(index\.html|README\.md|growth/(launch-checklist|show-hn-draft|product-hunt-draft|launch-alternativeto|2026-07-17-ecosystem-shares|shares)\.md|scripts/check-launch-facts\.mjs)$' || true)

# index.html + play.html share 893 hand-maintained identical lines (identical after the leading and
# trailing whitespace strip); 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, a 2-sided
# DIVERGENT edit 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)
# The sandbox matrix behind that guard: 16 mutations of the 2 surfaces, each with the verdict the
# guard must return — the 9 planned extractions of docs/twin-drift.md report 0 divergences (8 of them
# are fully silent; row 8 keeps the 5 one-sided deletions it cannot avoid), and a 2-sided DIVERGENT
# edit must still fail. It re-reads both surfaces, so it runs when the RULES move (the guard, the
# baseline or the matrix), not on every index.html edit.
touches_twindriftcases=$(printf '%s\n' "$staged" | grep -E '(^|/)(scripts/check-twin-drift\.mjs|scripts/check-twin-drift-cases\.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;
Expand All @@ -130,7 +142,7 @@ touches_llmsfull=$(printf '%s\n' "$staged" | grep -E '(^|/)(llms-full\.txt|llms\
# Nothing to check unless an .html file, one of the checkers, or a guarded doc changed.
# Every touches_* variable belongs in this chain — a gate left out of it never runs on a
# commit that stages only its own files, which is exactly the commit it exists to catch.
[ -z "$staged_html" ] && [ -z "$touches_launchfacts" ] && [ -z "$touches_njseditor" ] && [ -z "$touches_llmgate" ] && [ -z "$touches_export" ] && [ -z "$touches_login" ] && [ -z "$touches_sw" ] && [ -z "$touches_compat" ] && [ -z "$touches_run" ] && [ -z "$touches_jsparity" ] && [ -z "$touches_seedcache" ] && [ -z "$touches_imgports" ] && [ -z "$touches_langpages" ] && [ -z "$touches_editorports" ] && [ -z "$touches_settings" ] && [ -z "$touches_quickadd" ] && [ -z "$touches_i18n" ] && [ -z "$touches_updates" ] && [ -z "$touches_createapp" ] && [ -z "$touches_stale" ] && [ -z "$touches_vresume" ] && [ -z "$touches_lipsync" ] && [ -z "$touches_aresume" ] && [ -z "$touches_share" ] && [ -z "$touches_pricing" ] && [ -z "$touches_combine" ] && [ -z "$touches_pacing" ] && [ -z "$touches_graphpersist" ] && [ -z "$touches_graphsigs" ] && [ -z "$touches_describeapply" ] && [ -z "$touches_authlc" ] && [ -z "$touches_mediapriv" ] && [ -z "$touches_verhist" ] && [ -z "$touches_rungating" ] && [ -z "$touches_appsstorage" ] && [ -z "$touches_linkerr" ] && [ -z "$touches_deploy" ] && [ -z "$touches_hiddencss" ] && [ -z "$touches_connect" ] && [ -z "$touches_undo" ] && [ -z "$touches_costaccrue" ] && [ -z "$touches_resize" ] && [ -z "$touches_catalog" ] && [ -z "$touches_upscale" ] && [ -z "$touches_drift" ] && [ -z "$touches_promptcaps" ] && [ -z "$touches_consent" ] && [ -z "$touches_sharesamples" ] && [ -z "$touches_llmsfull" ] && [ -z "$touches_examples" ] && exit 0
[ -z "$staged_html" ] && [ -z "$touches_launchfacts" ] && [ -z "$touches_njseditor" ] && [ -z "$touches_llmgate" ] && [ -z "$touches_export" ] && [ -z "$touches_login" ] && [ -z "$touches_sw" ] && [ -z "$touches_compat" ] && [ -z "$touches_run" ] && [ -z "$touches_jsparity" ] && [ -z "$touches_seedcache" ] && [ -z "$touches_imgports" ] && [ -z "$touches_langpages" ] && [ -z "$touches_editorports" ] && [ -z "$touches_settings" ] && [ -z "$touches_quickadd" ] && [ -z "$touches_i18n" ] && [ -z "$touches_updates" ] && [ -z "$touches_createapp" ] && [ -z "$touches_stale" ] && [ -z "$touches_vresume" ] && [ -z "$touches_lipsync" ] && [ -z "$touches_aresume" ] && [ -z "$touches_share" ] && [ -z "$touches_pricing" ] && [ -z "$touches_combine" ] && [ -z "$touches_pacing" ] && [ -z "$touches_graphpersist" ] && [ -z "$touches_graphsigs" ] && [ -z "$touches_describeapply" ] && [ -z "$touches_authlc" ] && [ -z "$touches_mediapriv" ] && [ -z "$touches_verhist" ] && [ -z "$touches_rungating" ] && [ -z "$touches_appsstorage" ] && [ -z "$touches_linkerr" ] && [ -z "$touches_deploy" ] && [ -z "$touches_hiddencss" ] && [ -z "$touches_connect" ] && [ -z "$touches_undo" ] && [ -z "$touches_costaccrue" ] && [ -z "$touches_resize" ] && [ -z "$touches_catalog" ] && [ -z "$touches_upscale" ] && [ -z "$touches_drift" ] && [ -z "$touches_promptcaps" ] && [ -z "$touches_consent" ] && [ -z "$touches_sharesamples" ] && [ -z "$touches_llmsfull" ] && [ -z "$touches_examples" ] && [ -z "$touches_twindrift" ] && [ -z "$touches_twindriftcases" ] && exit 0
if ! command -v node >/dev/null 2>&1; then
echo "pre-commit: node not found, skipping HTML JS checks" >&2
exit 0
Expand Down Expand Up @@ -516,3 +528,54 @@ fi
if [ -n "$touches_llmsfull" ]; then
node "$root/scripts/check-llms-full.mjs"
fi

# 43) TWIN DRIFT: index.html (editor) and play.html (app player + export) carry 893 hand-maintained
# identical lines — identical after a leading/trailing whitespace strip, because play.html nests
# the same code deeper; only 415 of the 893 are byte-identical in the files. 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. It FAILS on a line that leaves the set because one surface
# moved and the other did not, on a line deleted from one surface while still live on the other,
# on a line that loses a copy on one surface only (the baseline stores per-surface occurrence
# counts, because a line can appear twice in one file), and on a line that leaves BOTH surfaces
# while each surface keeps its own DIFFERENT replacement of it. The count is a ratchet —
# extraction may lower it freely, new duplication may not raise it silently. The ratchet number
# is DERIVED from the baseline line list, and the required digest pins the per-line counts and
# the stored text, so no hand edit of the baseline can lift it.
# What leaves the set without failing: a line gone from BOTH surfaces where at most ONE surface
# grew a NEW near-identical replacement. The baseline stores, per line, the hashes of the lines
# that ALREADY looked like it, because a "replacement" that was there first replaced nothing —
# without that memory the guard failed 4 of the 9 planned extractions in docs/twin-drift.md.
# Generated regions are excluded, so the library is never booked as hand duplication; the
# njs-engine block is found by its data-hash, NOT by a script-tag regex that play.html's own
# export-builder string literal can trigger. Offline. Timings vary by machine and by load, and
# the load matters more than the code: each range below is scoped to the load band it was
# measured in and says nothing outside it. On 2026-07-29, over 30 samples of each case on a
# shared 18-core machine with the load average between 3.19 and 5.68, a clean tree took 0.14s to
# 0.18s. The worst case is a refactor that moves many twins at once: at the 200-departure
# classification ceiling it took 1.99s to 3.19s when every departure left both surfaces, and
# 0.91s to 1.64s when they left one. Never a hang:
# 201 departures reports totals instead. Each failure list NAMES its first 12 lines and counts
# the rest.
# 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

# 44) The sandbox matrix behind check 43. 16 mutations of index.html + play.html in a scratch copy,
# each with the verdict the guard must return: the clean tree, the 9 planned extractions of
# docs/twin-drift.md deleted from BOTH surfaces (8 silent, row 8 with the 5 one-sided reports it
# cannot avoid), a 2-sided DIVERGENT edit, a 1-sided edit, a 1-sided deletion, a correctly
# mirrored edit, and a fresh duplication. It exists because the guard's 2 directions pull against
# each other: the first divergence rule failed 4 of those 9 extractions, row 1 included, where
# the deletion is perfectly mirrored and sig = deletes = 25. Runs when the RULES change (the
# guard, the baseline, or the matrix), not on every surface edit: it runs the guard 16 times over
# mutated copies of 1.7 MB of HTML, which took 10.38s to 13.13s wall over 6 samples on 2026-07-29
# at a load average of 3.18 to 5.19, against the guard's own 0.14s to 0.18s. Load dominates that
# figure: an earlier 15-case run of the same matrix, at a load average of 23.5 and 6 GB into
# swap, took 65.9s — roughly 5 times today's slowest sample, on the same verdicts.
if [ -n "$touches_twindriftcases" ]; then
node "$root/scripts/check-twin-drift-cases.mjs"
fi
Loading
Loading