israerusan_unwritten/scripts
Israel Avila d3f0d0f278 Fix the NUL that made gaps.mjs undiffable, the slider combo that killed pairs, and a null-settings crash
src/core/gaps.mjs held a RAW NUL byte: pairKey wrote the pair separator as a literal 0x00
instead of an escape. Git classified the file as binary, so `git show --numstat` printed
"- -" and every change to it rendered as "Binary files differ" — the file holding all three
heuristics could never be reviewed in a diff again, and ripgrep/git grep skipped it. The
escape produces a byte-identical runtime string. test/source-hygiene.test.mjs asserts no
source file under src/, scripts/ or test/ contains a NUL, and pins the separator as a byte
no path can contain: with a printable one, ("A","B|C") and ("A|B","C") key the same and the
two pairs merge.

bridgeMaxLinks and maxOutlinksPerNote are independent sliders, so bridge >= cap was
reachable — at the cap slider's OWN MINIMUM, with the bridge slider at its default (6 vs 5).
Every note that survived the cap was then a bridge by definition, cooccur was never written
for any vault, and "Unexplained pairs" read "None found." forever with pairsConsidered: 0 and
no warning. The headline heuristic was silently off. resolveGapOptions now clamps the cap
above the bridge size; it only ever raises the cap, so a user's bridges stay bridges.

An explicit null string in data.json — a sync merge, a truncated write, a hand edit —
survived Object.assign (which only fills `undefined`), and refreshLicense's licenseKey.trim()
then threw inside onload(). The plugin did not degrade, it FAILED TO LOAD: no settings tab,
no commands, no way back without hand-editing JSON. Every string field is re-typed at load,
behind a `satisfies StringKeys<UnwrittenSettings>` list so a new string setting that is not
coerced is a compile error.

buildSnapshot retained every note's body for the whole run — ~160 MB of live JS strings on a
20k-note vault, on a plugin that ships isDesktopOnly:false and therefore runs on phones —
when only decision-tagged notes are ever read. Bodies are now retained only for decision
notes; word counts, which every note needs for stub hubs, are still computed for all.
snapshot.ts and gaps.mjs share one exported decisionSourceOf() so they cannot drift and
strand a decision note with no body (which would report it as unjustified — the worst
possible false positive).

community-plugins.json credited "saiken" while the manifest credits "Israel Avila". The
contract test pinned every other field, so the drift alarm had a hole in exactly the one that
disagreed; it now asserts author, and that the listed repo lives under the manifest's
authorUrl account.

Also re-synced src/shared from obsidian-plugin-core and committed src/shared/MANIFEST.sha256,
so `sync-shared --check` gates the vendored tree on a fresh clone.

Deliberately untouched: the Pro card (a later phase builds the features it advertises).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3f6GZ2A8FbZerhJEpJQBE
2026-07-14 12:35:40 -04:00
..
install-vault.mjs Unwritten 1.0.0 — report the notes you never wrote 2026-07-14 07:52:39 -04:00
sync-shared.mjs Fix the NUL that made gaps.mjs undiffable, the slider combo that killed pairs, and a null-settings crash 2026-07-14 12:35:40 -04:00