Building NICHLYST: 704 Red Assertions — Git Archaeology and the Repair of a Broken Data Contract
buildinpublic #gamedev #testing #javascript
It is November 7, 2026. The repository is split into 512 stages of work. Each stage is written as an action and a check that can fail. Stage 12 of that plan has one instruction: make node tests/game-data-integrity.test.js exit 0.
The suite prints its section headers one by one. Structure. Resources. Flags. And under each header, the failures accumulate — 704 red FAIL lines in total. Then, before the summary can print at all, the process dies:
tests/game-data-integrity.test.js:406 assertEqual(epilogue.variants, 33, 'Must have 33 epilogue variants'); ^ TypeError: Cannot read properties of undefined (reading 'variants') Something was missing from the data. And something else was hiding behind the crash.
This devlog is about finding both — with git, with proof, and with the discipline of not touching the test until the data had proven it wrong.
The Crash at Line 406
The suite reads the bundled game data:
const gameData = loadJSON(path.join(ROOT_DIR, 'game_data.json')); // line 76 // ... const epilogue = gameData.systems.epilogue_assembly; // line 405 assertEqual(epilogue.variants, 33, 'Must have 33 epilogue variants'); // line 406 → TypeError gameData.systems was not undefined. It had 24 keys. The one key the test wanted — epilogue_assembly — was simply not among them.
This distinction matters more than it looks. A wrong value fails an assertion. An absent value takes the whole process down with it. The sections behind the crash — narrative completeness, system config, accessibility, collectibles — never ran. A crashing test does not just fail a check. It erases the checks behind it.
What the 704 Was Made Of
Before the crash, the failures had already printed. Sorted by category:
| Category | Count | What it meant |
|---|---|---|
delta "truth" must be a valid resource | 566 | Moral-axis names sitting inside deltas, the resource container |
flag "..." must be declared (references) | 71 | Choices setting flags absent from global_flags |
Used flag "..." must be declared (unique) | 64 | Unique undeclared flags |
| Count mismatches | 3 | choices 82 vs 115, flags 130 vs 139, moral 114 vs 118 |
| Total | 704 |
Three numbers were simply stale. Two categories were real contract violations. And one category — the 566 — looked like a design decision that needed an answer: was the data wrong, or was the test wrong?
A Missing Node and a One-Way Mirror
www/game_data.json is not authored. It is generated:
// scripts/bundle_game_data.js:68 systems: meta.systems, The source of truth is www/data/meta.json. So the question narrowed fast:
node -e "const d=require('./www/game_data.json'); console.log(Object.keys(d))" # 13 top-level keys; systems = 24 keys; no epilogue_assembly grep -r "epilogue_assembly" www/ # 0 hits outside dist git grep "epilogue_assembly" HEAD # 0 hits Neither the bundle nor the source nor the current commit had the node. This made one thing brutally clear: patching the bundle alone would have been cosmetic. The next rebundle would have reproduced the loss. The repair had to land in meta.json.
Git Archaeology: Finding the Moment It Disappeared
Git keeps content, not intentions. So I asked the repository when the key had existed:
git log -S "epilogue_assembly" --oneline -- www/data/meta.json # 444ba77 feat: ... ← removal # 860c09d feat(monetization): ... ← addition Then I counted matches in the bundle at specific revisions:
| Revision | Matches in www/game_data.json |
|---|---|
bfa5de7 (parent of the removal) | 1 |
444ba77 (the removal) | 0 |
57929f1 (after the docs reorganization) | 0 |
The node had lived in the parent and died in the child. And the removed block, read straight out of the parent revision, was exactly what the test demanded:
"epilogue_assembly": { "sources": [ "record_status", "flags", "moral_ledger", "psychological_state", "hidden_postscript_flags", "archive_afterlife_flags", "codex_fragments", "memory_echoes" ], "variants": 33, "hidden_variants": 20 } Thirty-three variants. Twenty hidden. Eight sources. The test expected 33, 20, and at least five sources. Not approximately — exactly.
There was no redesign to do. The discipline was to restore the block byte for byte and let the assertion stay strict.
For context: commit 444ba77 recorded a batch of edits an earlier working session had staged wholesale. Nobody decided the epilogue should lose its configuration. A git add sweep simply took the working tree as it was.
What the Crash Was Hiding
With the epilogue node restored, the suite ran past line 406 — and kept finding absences. Four more system nodes had been removed by the same commit: dynamic_captioning, ethical_safeguards, director_memory, post_hidden_postscript. Each confirmed with its own git log -S. Two more followed: accessibility and privacy_analytics, plus project.codex_fragments_count.
Eight nodes restored in total. systems went from 24 keys to 31. Every value came from bfa5de7, where the test's expectations and the data still agreed.
The lesson is worth stating plainly: a crashing test is not a failing test. It is a stopped report. Whatever sits behind the crash is invisible — and invisible debt does not stay still.
Fixing the Data, Not Bending the Test
The plan that governs this work allows two moves per broken check:
- Restore the data, if the data is wrong.
- Change the test — only with proof written down first, in the previous stage's report, before any test line is edited.
For the 566 axis-named deltas, the proof had to come from the engine, not from intuition. Two files settled it:
// www/js/state_manager.js — choice pipeline const config = this.#gameData?.global_resources?.[resourceKey]; if (!config) continue; // unknown key: silently skipped Axis names — truth, survival, mercy, preservation, complicity, memory — have no entry in global_resources. The runtime has always skipped them. The real moral effects live in moral_config.json, read per choice by MoralLedger.js. The keys inside deltas were dead weight: harmless, misleading, and 566 instances of two subsystems drifting apart.
Removing them from eight day files — 1, 26, 36, 49, 44, 143, 133, 134, even one in the prologue — changed zero runtime behavior. That was the point: proven, not assumed.
For the three stale counts, the situation was inverted. The content had legitimately grown: 115 choices exist where the test demanded 82. An independent witness — the endings gate, 33 of 33 reachable — confirmed the canon was intact. So the test was re-baselined, and every edit cited its proof block:
- flags: 130 expected → 64 undeclared flags declared → 203 actual
- choices: 82 → 115
- moral-tracked choices: 114 → 118
- sound motifs: 6 → 12
-
paywallSrc.includes('Restore purchases')→includes('restorePurchases')— the flow moved from a UI label to an API call with five references in the source -
chapter_narrative_enaccepted as a flag-keyed object, becauseDayScreen.jsdocuments "supporting conditional flag-keyed objects" and days 4 and 14 ship branching prose
The temptation throughout was to make the test lenient. Accept any structure. Count whatever exists. The correct response was the same one this project keeps returning to: no. Restore the data. Declare the flags. Let the test stay strict.
Green You Can Trust
The result of stage 12:
Results: 4007 passed, 0 failed All game data integrity checks passed. Fifteen suites, all green — the first fully green run in the project's history. Before the repair, the score had been 14 of 15, with this file crashing every time.
And green is only worth what it costs to produce red. So the gates got negative tests:
- a CSS probe with
#FF0000→ the pre-commit hook rejects the commit (proven, then reverted) - an emoji injected into the bundle → the CI step exits 1 with a diagnostic (proven, then reverted)
- a planted forbidden word in the changelog → the terminology gate reports the file and the line (proven, then reverted)
The final state: npm run verify passes 7 of 7 gates — palette, endings, syntax, tests, build, emoji-free bundle, terminology. GitHub Actions: 18 of 18 green runs on main, zero failures. The pre-commit hook guards every commit since; CI guards every push.
One Stage at a Time
None of this is clever engineering. It is paperwork with teeth.
The project now runs on a written plan where every stage carries an action, a check that can fail, and a contingency branch for when it does. Stage 12 was supposed to produce a green test. It produced something better: a repaired contract between forty days of narrative, thirty-three endings, and the tests that guard them.
The missing key was epilogue_assembly — the assembly instructions for the afterlife itself. Part 8 of this series was about an interface that decays after the final ending. It turns out the configuration for that afterlife had been quietly deleted by an ordinary batch commit. Git remembered. The archaeology found it. The data came back exactly as it was.
Phase 1 of the plan — 32 stages — is now closed. The gates are green because they are allowed to go red.
One stage at a time. One check at a time. One restored node at a time.




