iGEM Chronology Puzzle: Provenance and Handoff (v0.541)

The iGEM Chronology Puzzle — Provenance & Handoff Document

Current version: v0.541. This document consolidates every prior provenance/changelog document in this project (igem-puzzle-v0.42-provenance.html, architecture-spec-multi-engine.md, clue-sets-B-and-C.md) plus everything learned since, into one place. It’s written to be read by a human or by a future Claude session picking this project back up cold — if you’re the latter, read §1 (current state) and §7 (engine implementation notes) first; those are the two sections most likely to save you from re-discovering bugs that have already been found and fixed.


1. Current state (read this first)

Executable page: igem-puzzle-v0.541.html. Single self-contained HTML file, no external dependencies, no build step. Opens directly in a browser or serves from any static host.

What works, fully verified (not just built): - Engine selection: Tau Prolog, ramo, or microKanren, chosen once per session before the main activity. - Clue-set selection: A, B, or C — all three clue-sets are available for all three engines (since v0.531); the setup screen doesn’t disable any (engine, clue-set) combination. - Every fact revealed comes from a genuine query against the selected engine — no clue’s readiness or result is decided by a JS conditional standing in for the engine. - A real Fitch-style proof (FitchFX, MIT) is driven automatically for 13 of Clue-set A’s 23 clues, when Tau Prolog is the engine (see §8) — still not wired to ramo or microKanren, and still not covering the remaining 10 clue types or clue-sets B/C. This is the one piece of the original plan still unfinished. As of v0.533, though, the interface now actually explains this coverage gap where it didn’t before (see below). - All nine (engine, clue-set) combinations converge on the identical real answer table (below), confirmed via jsdom end-to-end tests that click actual buttons and read the actual rendered DOM. - A dedicated provenance-tagging audit (v0.532, see §11) checked all 73 top-level declarations in the main script for attribution coverage and found/fixed three instances of stale “Clue-set A only” tag drift left over from the v0.531 port, plus one genuine tagging gap on the B/C clue-goal functions — see §11 for what was found and how. - Five interface/UX issues reported and diagnosed during v0.532 were fixed in v0.533 (§1a has the original diagnoses, §7e has how each was fixed): the broken FitchFX image, the missing Fitch-coverage and star-clue explanations, blocking Apply-button clicks, and a lack of local before/after feedback near each clue. Fixing the Apply-button responsiveness issue surfaced a genuine new concurrency bug (two clicks could launch overlapping queries against the same Tau Prolog session, confirmed to crash with an out-of-memory error) — also fixed, with an explicit lock, and documented in §7e as its own finding rather than folded silently into the original fix. - The Engine trace panel now serves a real purpose for all three engines, not just Tau Prolog (v0.534, see §7f) — it was reported as looking useless, and turned out to be useless permanently, not just for that one action: ramo and microKanren always returned trace: null, and the render function only ever checked that field, silently discarding a real query summary both functions were already computing. Fixed by building a genuine step-by-step log from data those functions already compute, rather than fabricating a resolution trace neither engine has any mechanism to produce. - A new Action-effect log (v0.541, see §7g) — a radical redesign, specified by the user: every clue application is now recorded, in order, along with every fact it revealed, including facts re-confirmed after already being known. The first-ever reveal of any fact is bolded; once all 24 facts are known, further actions are logged as wasted effort and the query is skipped entirely (a provable guarantee once that point is reached, not a heuristic). Required a real change to all three engines’ own fact-detection loops, not just a new display — see §7g for what changed and how it was verified not to alter any engine’s actual solving behavior.

The real answer (all nine (engine, clue-set) combinations solve to this):

Year Country Organism Project Award
2020 Netherlands Nematodes RootPatch Best Food & Nutrition nom.
2021 China Cyanobacteria (unnamed) Silver medal only
2022 USA Yeast Helo Best Therapeutics nom.
2023 Hong Kong Black Soldier Fly Ecoplaster Best Sustainable Dev. nom.
2024 Canada Spider silk ReneWool Gold medal, no nom.
2025 Germany Peptide-sensor bacteria SPROUT Safety & Security Prize

Immediate next steps (in the order they were being tackled when this document was written): the big one queued and explicitly deferred out of v0.533’s scope is §1b’s proof-coherence requirement (tier 1 minimal-support-set computation, then tier 2 full proof construction) — not started. Smaller and still open: extending the FitchFX translation itself to ramo and microKanren, to Clue-sets B and C, and to the organism-year block and the other 10 not-yet-covered clue types within Clue-set A. A useful periodic check for any future session, independent of new features: re-run the kind of provenance-tagging audit done in v0.532 (§11) every few versions, since tag drift (a comment describing something that used to be true) accumulates silently otherwise — and, per v0.533’s own experience, re-running the full regression suite after any change to click-handling or async structure is worth treating as mandatory, not optional, since that’s exactly the kind of change whose failure mode is a crash rather than a wrong answer.

1a. Known issues, noted during v0.532, fixed in v0.533

Five issues were observed in practice and diagnosed (root cause found and confirmed) during v0.532, then all five were fixed in v0.533. Kept here as a historical record of the diagnosis (per this document’s own convention of preserving history rather than deleting resolved notes) — see §7e for how each was actually fixed, and what was found while fixing them.

1. Broken image in the Fitch display section. Confirmed cause: FitchFX’s own header markup includes <img src="img/github.png" style="height:25px;"> (a GitHub link icon), vendored byte-identical along with the rest of FitchFX’s HTML/CSS/JS (see §8’s provenance note on the <iframe srcdoc> embedding). The six FitchFX .js files and one .css file were mechanically inlined into that srcdoc string, matching this project’s single-file-no-external-dependencies convention — but FitchFX’s own img/ asset folder (just the one GitHub icon) was never brought along, since inlining code doesn’t inline image files the same way. The <img> tag is still there, pointing at a relative path with nothing behind it, so it renders as a broken image. This is the only <img> tag found anywhere in the page (confirmed via a full-file search) — not a wider pattern, just this one leftover reference. Fix options for v0.533: either remove the <img> tag from the vendored srcdoc string (a from-upstream-copy deviation, which would need its own explicit tag noting the modification, since the current comment says “byte-identical… no new Fitch code” — removing even a broken <img> tag breaks that literal claim and should be flagged, not done silently), or inline the actual icon as a data URI (keeps the markup closer to byte-identical while fixing the display; small enough image that this is practical), or replace it with a plain GitHub-icon-free link (simplest, changes the vendored appearance slightly). No decision made yet on which approach to take.

2. Unclear why some clues have a Fitch derivation available and others don’t. Confirmed cause: FITCH_SIMPLE_A (the lookup table driving which clues get an automatic Fitch translation) only has entries for the 13 of Clue-set A’s 23 clues that fit one exact shape — a single organism-anchored antecedent implying a single consequent value in another category ({ante:['organism', X], cons:[category, Y]}), i.e. clues of the form “if the organism is X, [country/award/project] is Y.” The other 10 clues don’t fit that template for three different, unrelated reasons, none of which are explained anywhere in the interface: clues 1 and 20 are absolute position-pins with no antecedent at all (not an implication, so there’s no “if” to translate); clues 2, 3, 4, 5, 9, and 22 are relative- order or chained-existential clues (righto/aftero/multi-hop offset), which are a fundamentally different logical shape than “single value implies single value”; and clues 12 and 16 are country-anchored rather than organism-anchored pairings (the template only ever built an organism-anchored antecedent). None of this is a bug in the sense of incorrect behavior — every clue that doesn’t fit still works correctly as a puzzle clue, it just doesn’t get an automatic proof-panel translation — but it is a real gap in that the interface never tells the person playing why only some clues light up a Fitch derivation, so from the outside it looks arbitrary rather than principled. It’s also worth noting this coverage is Clue-set-A-and-Tau-Prolog-only regardless: ramo and microKanren never get Fitch derivations for any clue, and clue-sets B and C never get them either (see §8) — the “why” question likely covers both the intra-clue-set-A gap and this broader engine/clue-set restriction, and any v0.533 fix should probably address the user-facing explanation for both rather than just the narrower 13-of-23 case. Fix options for v0.533: at minimum, add a short explanatory note near the Fitch panel (something like “Automatic proofs are available for the 13 of 23 clues that translate to a single if-then rule; clues with absolute position pins, relative ordering, or multi-step offsets aren’t yet translated”) rather than silence; more ambitious would be extending FITCH_SIMPLE_A’s template to cover country-anchored pairings (12, 16) and absolute pins (1, 20) as straightforward variations on the existing pattern, which would leave only the genuinely-different relative-order/chained clues (2, 3, 4, 5, 9, 22) uncovered and explainable on their own terms. No decision made yet on how far to take this for v0.533.

3. No explanation for the star/orange-highlighted clues. Three of Clue-set A’s clues (9, 22, 23) render in orange with a ★, via wrap.className = 'clueRow' + (curData().text[num].includes('\u2605') ? ' star' : '') in renderClueRows() — the star is a literal character embedded directly in those three clues’ own text strings, not a separate flag. All three are independent ways to pin Hong Kong’s year (matching Clue-set A’s own top-level description: “23 clues, chained arithmetic + three independent routes to Hong Kong’s year”), but nothing in the interface says so — a person playing sees the color/star with no explanation of what it means or why those three specifically. Fix for v0.533: add a hint (tooltip, legend entry, or a line near the clue list) telling the person that clues marked with the orange/star styling are likely to be particularly load-bearing for finishing the puzzle, since each is an independent route to the same conclusion. Purely a UI-text addition — no logic changes needed, since the styling and the underlying three-independent-routes design are already correct and intentional, just unexplained.

4. Apply buttons feel unresponsive for ramo and microKanren (main-thread blocking, not background work). Confirmed root cause, not just perceived slowness: applyClueRamo and applyClueMK contain zero await statements in their bodies (verified directly by grepping each function’s body) — applyClueRamo is labeled async, but that alone does not create a yield point; a function only actually yields to the browser at a real await on a still-pending promise. Since neither function ever awaits anything, each runs its entire search (permutation generation, goal solving, diffing against known facts) as one uninterrupted synchronous block on the main thread. The click handler sets the button to “Querying…” before calling applyClue(), but the browser can’t paint that change until the current call stack returns — so for however long the query takes (measured up to ~2–3s for some ramo/microKanren B/C cases, see §7d), the button just looks frozen/unresponsive, then everything updates at once. This is the opposite of “background work”: the engine is blocking the only thread the browser has, foreground included. Tau Prolog doesn’t have this problem — runQuery() wraps Tau’s own callback-based session.answer() in a genuine Promise, which is a real yield point, so the browser gets to repaint before Tau’s heavy work runs. Fix options for v0.533: the simplest is inserting a real yield (e.g. await new Promise(r => setTimeout(r, 0))) between setting the “Querying…” state and calling into ramoLib.run(...)/the microKanren solve, so the browser paints the intermediate state first — this won’t make the query itself faster, but it will make the button visibly respond immediately rather than appearing stuck. A more thorough (and more involved) fix would move the actual search off the main thread entirely (a Web Worker), which would also stop the query from blocking any other UI interaction while it runs, not just this button’s own label.

5. No local feedback near the Apply button itself — the person has to look at a distant table to tell if a click did anything. Raised from a user’s perspective: the Known-So-Far grid is a different part of the page from the clue list, so after clicking Apply, there’s no way to tell from the button’s own area whether that click actually revealed something, without scrolling/glancing away and back. There’s a partial version of this already: when newFacts.length === 0, a .hint div gets appended right under the clue row explaining why nothing happened (see renderClueRows()’s click handler, and the noProgress entries in HINTS). But when a click does reveal something, nothing comparable appears locally — the person only finds out by checking the grid. Fix for v0.533: generalize that existing mechanism so it always shows local feedback, not just on the zero-progress case. Concretely: count total filled cells in KNOWN right before applyClue() is called (call it x) and again right after newFacts are folded in (call it y), and render a short message in the clue row’s own area either way — something like “Facts known before: x → after: y.” If y = x, that reads as “this click didn’t move anything forward” (subsuming the existing noProgress hint, which can stay as the more specific explanation when one exists); if y > x, it reads as immediate positive confirmation that the click was productive, without needing to look anywhere else on the page. Straightforward to build on top of what’s already there — the counting logic itself is trivial (Object.values(KNOWN).reduce((n,row) => n+Object.keys(row).length, 0), already used in exactly this form by the project’s own test scripts), the only new work is capturing that count before the query as well as after, and rendering it unconditionally rather than only in the zero-progress branch.

1b. Design requirement raised after v0.532: the reveal sequence should correspond to a coherent proof

The requirement, as stated by the user: when all facts are known, the sequence in which facts get revealed should correspond to at least one valid proof. Applying a clue should be a reasoning step (or a small number of them) expressible as logical statements — not an opaque result. This substantially broadens §1a issue #2 above: extending FITCH_SIMPLE_A’s template to more clue shapes would explain more individual clues, but it would not, by itself, guarantee the sequence of reveals forms a coherent proof. This needs its own analysis before v0.533 work starts, since it’s a genuinely different (larger) problem than the display-gap issue it grew out of.

Why the current architecture doesn’t satisfy this, mechanically: every engine’s applyClue* reveals facts the same way — query all currently-active clues plus already- known facts together, then check which category/row cells agree across every solution the engine returns, and report those as new facts. This is a diff against exhaustive search, not a step-by-step derivation. Nothing about it currently records why a cell became forced — only that it did. Three concrete gaps this creates: 1. Multi-clue combination. Several clues often only force a fact together, not individually — e.g. clue-set A’s clues 1–5 reveal nothing on their own but resolve the entire organism ordering once all five are active. A correct proof for that reveal needs to cite all five clues (and however the reasoning actually combines them), not just whichever one happened to be clicked last. 2. Order-dependence. Because clues can be clicked in any order, which facts are already “known” (and thus available as premises) at the moment a given clue is applied depends on the person’s own click order. A valid proof has to be built from what was actually known at that moment, not from some idealized canonical order — there’s no single fixed proof for the puzzle as a whole; there’s a different valid proof for each possible click order, and the code would need to construct whichever one actually happened. 3. Different clue shapes need different inference rules. The existing FITCH_SIMPLE_A template is a single-step modus-ponens shape (“if organism is X, category is Y”). That doesn’t cover: absolute position pins (which aren’t an implication at all — more like a direct premise); relative-order clues (righto/ aftero, needing something like transitivity chains over multiple known positions); chained-existential offset clues (clue 9/22-style, needing existential instantiation chained through intermediate positions); and — hardest — elimination-style reveals, where a fact becomes known only because it’s the one remaining possibility once every other value has been placed elsewhere (a disjunctive-syllogism/case-elimination argument, not a forward chain — in Fitch terms this typically needs nested subproofs with case splits, not a flat four-line derivation).

A two-tier plan, not yet started, for whoever picks this up: - Tier 1 (tractable, worth doing first): minimal-support-set computation. For each newly revealed fact, determine the smallest subset of currently-active clues (plus already-known facts) that still forces it — by re-querying with each active clue dropped one at a time and checking whether the fact still comes out forced; whichever clues can’t be dropped without losing the fact are the ones actually load-bearing for that reveal. This doesn’t construct a full natural-deduction proof, but it directly answers “which clues, combined, are actually responsible for this fact becoming known” — which is most of what’s needed for issue #2’s transparency problem, and a necessary input to tier 2 regardless. Feasible with the existing engines as-is (just more queries per reveal), and worth verifying its cost doesn’t reintroduce the kind of performance problems §7c/§7d already found and fixed for microKanren specifically. - Tier 2 (substantially harder): actual proof construction. Given a minimal support set from tier 1, construct a valid Fitch-style (or other natural-deduction) proof whose premises are exactly those clues/facts and whose conclusion is the revealed fact — this needs a distinct inference-rule template per clue shape (at least four: direct pin, single implication, order/transitivity, chained existential) plus a genuinely different template for elimination-style multi-clue combinations (nested case-split subproofs). Mining Tau Prolog’s own resolution trace (already partially read elsewhere in this project, per §7a’s debugger-trace note) is one possible source of structure for this, but SLD resolution and Fitch natural deduction are different proof formalisms — that translation is itself nontrivial, and wouldn’t help ramo or microKanren at all, which have no comparable trace to mine. Building tier 2 as an engine-agnostic layer on top of tier 1’s support sets (rather than trying to extract it from each engine’s internals differently) is probably the more robust path, but this is genuinely new proof-search code, not a lookup-table extension like FITCH_SIMPLE_A.

Scoping note: this is larger than a single v0.533 session’s worth of “extra code,” on the order of the original three-engine or three-clue-set efforts (§6/§7d) rather than a bug fix. Worth explicitly scoping/estimating tier 1 alone before committing to tier 2 in the same version, rather than assuming both fit together.


2. File inventory

Note on the jump from v0.534 to v0.541: earlier versions incremented by exactly 0.001 per round of changes; v0.541 is a user-specified version number for this particular (substantial, “radical redesign”) change, not a sign that versions 0.535–0.540 exist somewhere and were skipped over in this document. There is no gap to account for.

Executable pages (all preserved, never overwritten)

File What it is
igem-puzzle-v0.1.html through v0.32.html Early versions: three-engine demo, Tau-only debugger, JS-checker panel, first-order-logic notation, layout refinements. Passive (run-to-completion), not the “student must click, nothing revealed until earned” design.
igem-puzzle-v0.2-provenance.html Standalone code-provenance page for v0.2 specifically (vendored-vs-authored line breakdown).
igem-puzzle-v0.4.html, v0.41.html First “forcing” interaction model — student must click, but backed by hand-written JS logic, not a real engine. v0.41 fixed a real bug (v0.4 revealed cells that individual clues hadn’t actually earned).
igem-puzzle-v0.42.html Full hand-authored Fitch-proof engine (reductio boxes, ∀-Elim/MP split, plain-English panel) — entirely bespoke Claude logic, no real theorem-proving engine underneath. Superseded architecturally by v0.5+, but the FOL notation scheme (single-letter predicates, codes) carries forward.
igem-puzzle-v0.42-provenance.html The changelog/attribution page for the whole project up to that point — fully consolidated into this document; no longer needs separate consultation, though it still exists and its per-line vendored-code diffing for v0.42 specifically is not repeated here.
igem-puzzle-v0.5.html First genuinely engine-driven version. Tau Prolog, Clue-set A only, but every fact from a real query. Found and fixed two real bugs (a permutation/2-anchoring bug; a 31,815-step trace from capturing the wrong query).
igem-puzzle-v0.51.html Removed the “apply clues 1-5 together” special-case button — every clue became individually clickable and queryable, with hints explaining engine+clue-set-specific quirks. Found and fixed a severe bug: naive query ordering caused a 45+ second hang and once an out-of-memory crash; fixed via dynamic goal ordering (most-constrained category first) plus a documented, performance-justified readiness gate.
igem-puzzle-v0.52.html Generalized to three clue-sets (A/B/C), all Tau Prolog, all independently verified.
igem-puzzle-v0.521.html Same functionality, fully annotated in-code with [USER INSTRUCTION]/[CLAUDE INITIATIVE]/[USER CONTENT]/[OPEN SOURCE] tags (see §9). Fixed an unrelated pre-existing display bug found while annotating (a literal \u2013 string in the footer instead of an en-dash).
igem-puzzle-v0.522.html + igem-puzzle-v0.522-fitch.html Added a Fitch-proof section using ndt93/Proof-Editor (MIT) as a separate, disconnected file — student manually re-types facts into it. Superseded by v0.524’s FitchFX integration (below), but the two-file version is kept as a working fallback.
igem-puzzle-v0.523.html Merged the two v0.522 files into one, embedding ndt93/Proof-Editor via <iframe srcdoc="...">. Functionally fine but poor UX (fully manual, disconnected) — motivated the switch to FitchFX.
igem-puzzle-v0.524.html + igem-puzzle-v0.524-fitchfx.html Replaced ndt93/Proof-Editor with mrieppel/FitchFX (MIT) — genuine first-order logic support, real quantifier rules. Built a translation layer (Claude-authored, but pure data-formatting, not new Fitch logic) that converts the Tau Prolog engine’s real derivations into FitchFX’s plain-text import format, which FitchFX’s own unmodified parser/checker/renderer then validates and draws. Covers 13 of Clue-set A’s 23 clues (the “simple” ∀-Elim + Modus Ponens pattern) — see §8 for exactly which, and what’s not covered yet.
igem-puzzle-v0.525.html Added ramo as a second engine (Tau Prolog remains the first), Clue-set A only. Real bugs found building this: a permutation/2-anchoring bug carried over from earlier fixes; the RAMO_VARNAME capitalization bug (§7b).
igem-puzzle-v0.53.html Added microKanren as a third engine, Clue-set A only — a from-scratch, from-the-paper core (Hemann & Friedman), not a vendored package (see §7c/§9). Real bugs found building this: a single-goal Array.reduce bug in the core itself, a goal-ordering performance blowup, and a cold-query OOM crash requiring the same class of readiness gate Tau needed. See §7c for all three.
igem-puzzle-v0.531.html Ported ramo and microKanren to Clue-sets B and C — all three engines now support all three clue-sets (nine combinations total, all verified). Two new relations needed for the port (offsetK, generalizing clue-set A’s existing chained-righto pattern to arbitrary offsets; pairedAtoAmong, a complement-set translation of clue-set C’s negation-as-failure clues for engines with no negation primitive), built once and reused by both ramo and microKanren. See §7d for details, and the fixed stale version footer that had been left over from v0.525.
igem-puzzle-v0.532.html Two changes: (1) an interface change — every clue’s Apply button now has a nearby “Clicked: N” label, starting at 0 and incrementing once per click, stored outside the render function so it survives renderClueRows() rebuilding the row’s DOM after every click; (2) a dedicated provenance-tagging audit (see §11) that found and fixed three instances of stale “Clue-set A only” tag drift left over from the v0.531 port, plus one genuine tagging gap on the B/C clue-goal functions.
igem-puzzle-v0.533.html Fixed all five issues queued in §1a: the broken FitchFX GitHub icon (inlined as a data URI, fetched from FitchFX’s own upstream repo); added a real explanatory note near the Fitch panel (also fixing a dangling self-reference that had promised a note which never existed); explained the star/orange clue highlighting in the same note; added a real yield point so Apply buttons visibly respond before a heavy query runs; and added local “Facts known before → after” feedback in each clue row. Also found and fixed a genuine concurrency bug introduced by the yield-point fix itself — see §1a’s updated notes and §7e below for the full account, including two of the project’s own test scripts that needed the same kind of fix.
igem-puzzle-v0.534.html The Engine trace panel (“Engine trace (last action)”) reported showing nothing useful whenever Tau Prolog wasn’t selected — confirmed as a real, permanent gap: ramo and microKanren both always returned trace: null, and renderTrace() only ever checked that one field, so it fell back to a generic “No trace recorded” message on literally every action for those two engines, discarding a res.query summary string those functions were already computing. Fixed by having both engines’ applyClue* functions build a genuine step-by-step log (res.steps) from data they already compute — which categories were pinned vs. searched, and which clues fired, in order — and having renderTrace() actually use it instead of falling through to the dead message. See §7f.
igem-puzzle-v0.541.html Current version. A radical redesign, user-specified: a new Action-effect log tracks every clue application, in order, across a single global action counter — not just which facts are newly discovered, but every fact each query re-confirms too, even ones already known. The first-ever reveal of any fact is bolded; later re-reveals of the same fact say which numbered time it’s been revealed. Once all 24 facts are known, further actions are logged as “wasted effort” and the query is skipped entirely rather than run pointlessly (a provable guarantee, not a heuristic). Required changing the fact-detection loop in all three engines’ applyClue* functions — previously each one skipped already-known cells outright, so there was no way to tell whether a query had re-confirmed anything. See §7g for the full account, including why the “wasted effort” check could safely live in the shared dispatcher rather than duplicated per engine.

Documents

File What it is
architecture-spec-multi-engine.md The original three-engine design plan (Tau/ramo/microKanren behind one shared interface). Still useful for the microKanren section (§3c there) and the “trace panel deliberately not normalized” design principle (§5 there) — fully consolidated here otherwise.
clue-sets-B-and-C.md Full clue text, FOL, and verification notes for Clue-sets B and C. Fully consolidated into §5 below; the original documents the 4 real bugs caught constructing those clue-sets, repeated here in §5.
This document Supersedes the above two for changelog/handoff purposes. The prior documents are not deleted and remain individually readable if needed, but this is the one to read first.

Standalone test/module files (project sandbox, not shipped in the HTML)

File What it is
mk-backend.js The standalone microKanren core (transcribed from Hemann & Friedman), built and verified before HTML integration — same discipline as ramo-backend.js before it.
mk-puzzle.js The puzzle relations (membero/righto/aftero/nthValo/pairedAto) and clue encoding built on mk-backend.js, plus solve()/isReadyMK().
test-mk-core.js Correctness tests for the bare core primitives (unify, membero, righto, aftero, nthValo, pairedAto) before any puzzle logic was layered on.
test-mk-puzzle.js Full 23-clue sequential playthrough against mk-puzzle.js, with per-clue timing — this is where the goal-ordering performance bug was first found and the fix verified.
test-mk-stress.js The cold-query stress test that found the OOM crash (two/three/four categories fully unconstrained at once) that motivated the readiness gate.
test-mk-readiness.js Verifies the >=5/6-known threshold is both necessary (4/6 still risky) and sufficient (5/6 resolves fast) — same empirical-boundary discipline as Tau’s own threshold.
test-mk-distinctness-gap.js Direct verification that bare microKanren has no native disequality primitive, confirming the permutation-list fix is necessary rather than precautionary.
run-mk-integration-test.js jsdom test clicking real buttons against the real, integrated page with microKanren + Clue-set A selected — originally written against igem-puzzle-v0.53.html, repointed at igem-puzzle-v0.531.html once that became current (same test, just a newer target file).
run-full-regression.js Full regression across every supported (engine, clue-set) combination in the integrated page, run after the microKanren integration to confirm Tau and ramo still work and unsupported combinations are still correctly gated off; extended in v0.531 to cover all nine combinations once B/C support was added to ramo and microKanren, replacing its earlier “confirm still disabled” checks for those combinations.
ramo-shared.js / ramo-puzzle-bc.js Standalone ramo relations (including the new ramo_offsetK/ramo_pairedAtoAmong) and B/C clue encodings, built and verified before HTML integration for the v0.531 port.
test-ramo-bc.js Full sequential playthrough for ramo’s B and C encodings — found no bugs beyond expected “a few seconds” timing, consistent with clue-set A’s ramo experience.
test-ramo-bc-stress.js Cold-query stress test confirming ramo’s B/C clues stay fast even fully unconstrained, same as clue-set A — no readiness gate needed for ramo on any clue-set.
test-mk-bc.js Full sequential, readiness-gated playthrough for microKanren’s B and C encodings.
test-mk-bc-stress.js Cold-query stress test confirming microKanren’s B/C clues do OOM when cold, same as clue-set A — confirms the existing readiness gate is genuinely load-bearing for the new clue-sets too, not just decorative.

3. What this project actually is

An interactive puzzle tool built around a 6×4 Einstein/zebra-style logic puzzle (6 iGEM synthetic-biology teams, 2020–2025, each with a unique country/organism/project/award). The puzzle itself was supplied by the user at the very start of the project (real, sourced iGEM team data) — this is user-supplied content, not something Claude invented.

The project’s guiding constraint, stated explicitly partway through and driving every architectural decision since: maximize genuinely open-source or textbook-derived code, minimize bespoke Claude-authored logic, for presentation to judges as evidence of responsible AI use. This is why the architecture pivoted hard partway through (see §4) — an earlier, more pedagogically elaborate design (v0.4–v0.42) was almost entirely bespoke JavaScript, which directly conflicts with that goal even though it was a reasonable design on its own terms.


4. The architectural pivot (why v0.5 looks nothing like v0.42)

v0.1–v0.32: mostly open-source-dependent (Tau Prolog / ramo / microKanren doing the actual solving) but passive — click “solve,” watch a trace.

v0.4–v0.42: maximally forcing (nothing revealed until the student clicks for it — including a real hand-authored Fitch natural-deduction engine with genuine reductio boxes) but almost entirely bespoke Claude logic. None of the readiness-checking, case-exhaustion analysis, or Fitch-proof construction in these versions came from Tau Prolog, ramo, or microKanren — it was Claude re-implementing what those engines could have done, defeating the “minimize bespoke code” goal even while nailing the pedagogy.

v0.5 onward: the resolution. Keep the forcing function (nothing revealed until genuinely derivable), but make the engine the thing doing the deriving — “is this clue ready” becomes a real query against whichever backend is selected, using that engine’s actual unification/search, not a hand-written JS conditional. This is a genuinely different codebase from v0.42’s, not an incremental evolution of it; the two lineages share almost no logic (only the FOL notation scheme carries forward).


5. The three clue-sets

All three use the identical vocabulary (6 years, 6 countries, 6 organisms, 6 projects, 6 awards) and the identical organism↔︎project pairing (real project names tied to organisms: RootPatch↔︎nematodes, Helo↔︎yeast, ReneWool↔︎spider silk, Ecoplaster↔︎Black Soldier Fly, SPROUT↔︎peptide-sensor bacteria, the unnamed algae project↔︎cyanobacteria — fixed across all three, not re-derived per clue-set). All three converge on the identical real answer (§1). They differ in reasoning style:

Pairwise clue overlap (literal text comparison against A’s exact wording, not estimated): A∩B = 3 clues (share the same natural, singular wording for “the nematode team was Dutch,” “the American team was nominated for Best Therapeutics,” and “the German team won the Safety and Security Prize” — deliberately not forced apart into awkward alternate phrasing). A∩C = 0. B∩C = 0. All well within the requested ≤8-per-pair budget.

Four real bugs caught building B and C, all found by running the actual solver, not by re-reading the hand-written clues more carefully: 1. Clue-set C’s project-exclusion clues originally excluded only 4 of 5 wrong answers (forgot the unnamed algae project as a candidate), leaving every project ambiguous. 2. A leftover dead-code line (nth1(Anem, Awards, XX0), true) nondeterministically enumerated award positions, producing 6 spurious duplicate “solutions.” 3. An unbound-variable typo (Nnem referenced but never bound to the right quantity) silently misapplied one constraint. 4. Clue-set B and C’s cyanobacteria-year clues were accidentally worded identically before this was caught and fixed (would have been an unintended overlap).

Full clue text and FOL notation for all three sets are embedded in igem-puzzle-v0.525.html itself (CLUESET_DATA.A/B/C.text and .fol), and the original verification notes are also in clue-sets-B-and-C.md.


6. Architecture: the multi-engine design

Core idea: one UI shell (clue list, known-so-far grid, trace panel) that contains no solving logic itself. All solving logic lives behind a shared informal interface, implemented once per engine. The student picks engine + clue-set once per session; everything downstream is identical regardless of which combination they picked (subject to the combination actually being supported — see §1).

UI Shell (clue text, grid, trace panel, buttons)
        |
        v
  per-engine dispatch (isReady / applyClue, see §7)
        |
   -----+---------+-------------
   |               |            |
Tau Prolog        ramo      microKanren
 (built, A/B/C)  (built, A/B/C) (built, A/B/C)

Why session-level, not per-action: switching backends mid-derivation would require translating partial state between fundamentally different internal representations (a Prolog substitution vs. a relational substitution map) — real complexity for no real benefit. Session-level choice keeps each backend self-contained and independently testable, which mattered a great deal in practice (see §7).

Shared puzzle data (used by all backends, never used to decide correctness — only for display and for cross-checking test results): the clue text, display labels, and the answer key.

The trace panel is deliberately not normalized across engines. Tau Prolog’s trace is its own debugger_states (real WAM resolution steps); ramo’s is a textual description of the query run (no per-step hook exists — see §7b); FitchFX’s proof panel is a third, different kind of “trace” entirely. Forcing these into one common shape would hide real information about how each engine actually works, which cuts against the whole point of showing genuine internals.

microKanren, built in v0.53. Per the original plan, this is a from-scratch, from-the-paper core (Hemann & Friedman), not a port of any existing JS package — see §7c for the implementation notes and §9 for why its provenance category is distinct from Tau Prolog’s and ramo’s (transcribed technique vs. vendored dependency).


7. Engine implementation notes — read before touching either backend

7a. Tau Prolog (built: Clue-sets A, B, C)

7b. ramo (originally built for Clue-set A only in v0.525; ported to B and C in v0.531, see §7d)

7c. microKanren (originally built for Clue-set A only in v0.53; ported to B and C in v0.531, see §7d)


7d. Porting ramo and microKanren to Clue-sets B and C (v0.531)

Goal: all three engines support all three clue-sets. Built and verified in standalone modules first (ramo-shared.js/ramo-puzzle-bc.js and the B/C additions to mk-puzzle.js, plus test-ramo-bc.js/test-ramo-bc-stress.js and test-mk-bc.js/test-mk-bc-stress.js), same discipline as every other engine addition, before touching the HTML.

7e. Fixing the five §1a issues (v0.533), and a real concurrency bug found along the way

All five issues noted during v0.532 (§1a) were fixed in v0.533. Four were small and self-contained; the fifth (the Apply-button responsiveness fix) surfaced a genuine new bug that needed its own fix on top of the original one.

A real regression found while testing issue #4’s fix, not a separate pre-existing bug: before this fix, every engine’s applyClue* ran as one uninterrupted synchronous block — which, entirely by accident, meant only one query could ever be in flight at a time, since nothing yielded control back to the event loop mid-query. Adding a genuine yield point removes that accidental mutex: two Apply clicks close enough together can now both reach their query call before either resolves, launching two overlapping queries against the same shared Tau Prolog SESSION object. Confirmed directly, not just suspected: a from-scratch sequential-clicking test crashed with an out-of-memory error at exactly the point two queries would have overlapped (specifically, Tau Prolog + Clue-set A, clue 10, when country and award were both still fully open) — the identical test without the yield-point fix never crashed there. Tau’s session isn’t designed for concurrent queries, so the yield-point fix on its own was actually unsafe to ship without a second fix alongside it: a QUERY_IN_FLIGHT lock, checked and set at the top of the click handler, that disables every Apply button (not just the one clicked) for the duration of a query, and is read by renderClueRows()’s own disabled-state logic so any render respects it. Verified this actually fixes the crash (the same from-scratch test that crashed before no longer does), and re-ran the full 9-combination regression suite afterward to confirm no other combination regressed.

Two of the project’s own test scripts needed the same kind of fix, for the same reason: run-full-regression.js and test-click-count.js both originally waited a short fixed delay after dispatching a click before assuming it had fully resolved (30ms and 150ms respectively) — safe under the old fully-synchronous behavior (a click was guaranteed complete by the time any fixed delay elapsed), not safe anymore now that there’s a genuine yield point and an explicit lock gating completion. Both were rewritten to poll for the button set actually re-enabling (doc.querySelector('#clueRows button:not([disabled])')) rather than guessing a sleep duration — this is also the more correct way to write a test like this regardless of which engine is being exercised, since Tau’s exhaustive findall on Clue-set B/C’s later clues can genuinely take longer than ramo’s or microKanren’s equivalent queries, and a fixed guess that happens to work for one engine isn’t guaranteed to work for another.

7f. The Engine trace panel was dead UI for two of three engines (v0.534)

Reported directly (screenshot showing “No trace recorded for this action” after applying a clue on Clue-set C): the “Engine trace (last action)” panel appeared useless. Confirmed it wasn’t just that particular action — it was every action, for two of the three engines, permanently:

Fix: rather than fabricate step-by-step resolution the way Tau’s debugger produces (neither engine has any mechanism that could genuinely produce that), both applyClueRamo and applyClueMK now also build res.steps — a real, accurate, numbered log constructed from data they already compute during goal construction: one line per category (whether it was pinned outright from already-known facts, or searched over its 720 permutations and how many positions constrained it), one line per active clue that fired (its number, its text, and which categories it touches), and a final line with the result count and elapsed time. renderTrace was updated to check res.trace first (Tau’s real resolution steps, unchanged), then fall back to res.steps (the new per-engine goal-construction log) before finally falling back to the generic message — so that generic message is now reachable only when neither exists, rather than being the default outcome for two-thirds of the engine options.

Verified directly (test-trace-panel.js): clicking a clue on ramo and on microKanren now shows genuine, non-fabricated content in both the header and body of the trace panel, where before both always showed the same static fallback text regardless of which clue was clicked. Confirmed the numbers in that content are real by cross-checking them against each engine’s own already-established behavior (e.g. microKanren’s step count matching its genuinely-exhaustive solution count, not a sampled one, consistent with §7c’s documented distinction from ramo’s bounded run()). Full 9-combination regression suite re-run afterward, still all-pass.

7g. The Action-effect log (v0.541, radical redesign, user-specified version number)

The requirement, as specified by the user, nearly verbatim: track which rule applications reveal facts, even facts already revealed before — present this as a log where the first-ever reveal of any fact is emphasized (bold), and once every fact is known, further actions are logged as wasted effort.

Why this needed real changes, not just a new display: every engine’s fact-detection loop (applyClueTau/applyClueRamo/applyClueMK) had always skipped already-known cells outright — if (KNOWN[r][cat]) continue; — because the only thing that mattered before was newFacts (genuinely new information to merge into KNOWN). That skip meant there was no way to tell whether a query had re-confirmed an already-known fact or simply never touched that cell at all; the information the new feature needs didn’t exist anywhere to log. Fixed by removing the skip in all three loops: every forced cell is now recorded into a new revealed array (a superset of newFacts), and newFacts is derived from it by filtering to cells not already in KNOWN — same final behavior for everything that already depended on newFacts (verified via the full regression suite, unchanged), with the strictly-additional revealed array carried alongside it in each function’s return value.

New session-level state (same never-reset-mid-session lifetime as KNOWN/ACTIVE/ CLUE_CLICK_COUNTS — no “back to setup” control exists on this page): - ACTION_LOG: one entry per Apply click, in the single global order they happened, each listing every fact that click’s query forced (or a wasted: true marker). - ACTION_COUNTER: a 1-indexed action number across the whole session — distinct from CLUE_CLICK_COUNTS, which counts clicks per individual clue; this counts actions in the single sequence the log presents. - FACT_REVEAL_COUNT: keyed by "row-field", incremented every time that fact is revealed by any action. A fact’s first-ever reveal is exactly when this reads 1 after incrementing — the same event KNOWN newly acquiring that fact marks, tracked by a separate counter because the feature needs an actual count (“revealed 2nd time”), not just a yes/no.

The wasted-effort check lives in the shared applyClue() dispatcher, not duplicated per engine: once every one of the 24 knowable facts (6 rows × 4 categories — the same ceiling renderKnownGrid’s own comment already establishes) is in KNOWN, no further query against any engine could possibly reveal anything else. This is a mathematical guarantee, not a heuristic, so it was implemented as an early return in applyClue() itself — before any engine-specific function is even called — which has the added benefit of skipping a pointless query entirely once the puzzle is solved, rather than running (and paying for) a full search that’s provably going to find nothing new. This matters more for ramo’s and microKanren’s heavier searches than Tau’s, but applies to all three uniformly since the check is engine-agnostic.

Rendering: a new panel (“Action-effect log”) was added to the main layout, below the existing Engine trace panel, with its own render function (renderActionLog()) called after every click and once at session start. Unlike the plain-text trace panel, this one uses innerHTML (not textContent), since the whole point is bolding first reveals — each action’s entry is either a “Wasted effort” line, or one line per revealed fact, phrased as “First reveal of row R (year), Category = value.” (bold) or “Re-revealed row R (year), Category = value (revealed Nth time).” (plain), using a small hand-written ordinal-number formatter for the “Nth” phrasing.

Verified directly (test-action-log.js), not just by inspection: clicking a clue for the first time produces a bolded first-reveal entry; clicking the same clue again (after that fact is already known) produces a plain “re-revealed… 2nd time” entry, not another bolded first-reveal; playing through an entire clue-set to all 24 facts known and then clicking once more produces a “Wasted effort” entry. Full 9-combination regression suite re-run afterward, still all-pass, confirming the fact-detection loop change didn’t alter any engine’s actual solving behavior — only what gets recorded about it.

Relationship to §1b’s proof-coherence requirement: this is a genuinely different, smaller piece of work than §1b’s tier 1/tier 2 plan — the action-effect log records what was revealed and when, across the literal sequence of clicks, but doesn’t construct or verify that the sequence forms a valid proof (no inference rules, no minimal-support-set computation, no case-split reasoning). It’s a useful complementary piece of infrastructure for that future work (a per-action list of exactly what changed is a natural input to a support-set computation), but doesn’t substitute for it — §1b remains open and out of scope for this version.

8. The Fitch proof panel (FitchFX integration)

History: two candidates were researched and one was abandoned mid-integration once a better match was found — worth knowing both, since the abandoned one (ndt93/Proof-Editor) is still sitting in the outputs directory as a working two-file fallback (v0.522/523).


9. Open-source dependencies (complete list, with license and role)

Library License Role Vendoring
Tau Prolog BSD-3-Clause The Prolog engine backend. core.js + lists.js modules, inlined verbatim, byte-diffed against the pristine npm package before shipping.
ramo MIT (Copyright 2019 William Lewis) The relational (miniKanren) engine backend. membero/righto/aftero copied from its README tutorial. dist/bundle.umd.js, inlined verbatim.
FitchFX MIT (Michael Rieppel) The current Fitch-proof display/validation engine. Six .js files + one .css file, mechanically inlined into one HTML file; logic completely unmodified.
D3.js (v3-era, vendored by FitchFX itself) BSD-3-Clause (Copyright 2010–2015 Michael Bostock) FitchFX’s own SVG drawing dependency. Inlined as part of vendoring FitchFX.
ndt93/Proof-Editor MIT The original Fitch-proof tool (v0.522/523), superseded by FitchFX but preserved as a working fallback. Propositional logic only. index.html + 4 .js files + 1 .css file, mechanically inlined.
microKanren N/A — transcribed technique, not a vendored package The relational (bare miniKanren-core) engine backend, built in v0.53. A from-scratch JS transcription of Hemann & Friedman’s reference implementation (MIT-licensed paper), not copied from any existing JS microKanren port.

Researched but explicitly rejected, for future reference (don’t re-research these): - OpenLogicProject/fitch-checker and its successor LogicPenguin (Kevin Klement) — both GPL-3.0, ruled out by the permissive-license-only constraint. - Carnap (carnap/carnap) — GPL-3.0, same reason. - dramala/prolog-Natural-Deduction — no license file at all (defaults to all-rights-reserved), ruled out regardless of content. - A dedicated “translate a Prolog resolution trace into a Fitch proof” open-source tool was searched for directly and not found with a permissive license — the closest match (a 2021 bachelor’s thesis, “Towards Automated Natural Deduction in Prolog”) does something adjacent but different (searches for proofs from scratch, doesn’t translate an existing trace), and its code availability/license is unconfirmed. This is why the FitchFX translation layer (§8) is Claude-authored data-formatting rather than a found open-source translator — there wasn’t one to find. - shimmer (BSD-2-Clause) and cowboy/javascript-hooker (MIT) were identified as good candidates for instrumenting ramo/microKanren with real open-source function-wrapping code (see the original architecture spec §8), intended for adding a trace/logging capability to those backends. Not yet actually vendored or used — ramo’s current trace (§7b) is just a descriptive string, not instrumented via either of these. Worth revisiting if a richer ramo trace is wanted later.


10. In-code provenance tagging convention

Established in igem-puzzle-v0.521.html and maintained since. Every block of original code/content in the final (non-vendored) <script> section is tagged with one of:

As of v0.532 (current), the exact tag count in the main script (not a rough estimate — counted directly during the v0.532 audit) is: 25 [USER INSTRUCTION], 68 [CLAUDE INITIATIVE], 8 [USER CONTENT], 11 [OPEN SOURCE]. When extending the code, keep tagging as you go, in the same pass as writing the code — two audit passes now (v0.521 and v0.532) have each found real gaps or drift that accumulated silently between them, which is more error-prone than tagging immediately and checking periodically.


11. Testing discipline (the pattern to keep using)

Every version in this project has been verified by actually running it — clicking real buttons in a real (simulated) DOM via jsdom, reading the actual rendered output — not by code review alone. This discipline is why so many real bugs got caught before shipping rather than after:

When extending this project: write a standalone test script, run it, read the actual output before believing it, and re-run the full existing regression suite (not just the new piece) after every change. The test files (run-v52*-test.js and similar) in the project sandbox show the established pattern — reuse and extend them rather than starting from scratch each time.