The following is an architecture report on experimental software built with Fable. Parts of this report have been slightly altered to obscure what is being built, but no material findings have been changed or omitted.
The codebase is small (roughly 10K lines of code) and Fable had no significant problems in creating the software. To create the original spec and plan, we used Superpowers and PAAD to ensure that both were of the highest quality. In other words, we used spec-driven development with an extremely detailed, high-quality specification.
For the implementation, we deliberately did not use the PAAD methodology. We wanted to assess the quality of the software that Fable can natively build without engineering assistance.
After the implementation, we manually verified that the software behaved as intended. It mostly did, with a few quirks, but it was suitable. Then we ran PAAD's /agentic-architecture tool to generate the following report. This spawns multiple subagents, each specializing in a particular kind of architectural area, and a final validator to deduplicate and verify the findings. We then used /pushback to validate. It's clean and correct.
The final verdict is damning. Fable can brute force past architectural challenges, but without clear and explicit guidance, it creates the same technical debt that less powerful models create. Thus, Fable does not produce production ready code. You can jump straight to the flaws, if desired (reading full report goes into detail on them).
Counts: 17 verified strengths (7 High, 9 Medium, 1 Low) and 33 verified flaws (4 High, 17 Medium, 12 Low), distilled from 79 raw findings by 5 specialist agents plus an adversarial verification pass. For a small, 10K LOC codebase that's built in a few hours this is terrible. The PAAD methodology avoids these flaws and builds production-ready code, but at a higher unit cost (tokens+time). This is offset by lower TCO — less rework, and fewer bugs or security holes.
Date: 2026-07-18
Commit: 06a5cf9663ba6be71c99606dc6dd5ceaadb891e3
Languages: [REDACTED]
Key directories: src/sim/ (deterministic core), src/state/ (sim↔React bridge), src/ui/, data-raw/, scripts/, docs/adr/
Scope: full repository
[REDACTED]. ~65 source files, ~10.5k lines, 31 commits, 16 ADRs. Architecture is three layers with a declared strict dependency direction sim → state → ui: a headless deterministic sim core (event-sourced — the save file is the [REDACTED] action log, rewalked via rewalkScenario()), a thin state bridge (module-level SimScenario + Zustand mirror + fixed-timestep rAF loop), and a [REDACTED]. Ephemeris data is baked at build time from committed JPL Horizons JSON.
Note: all 33 flaws below survived verification, slightly exceeding the template's 25-item guideline; none were cut.
- Category: S13 Domain modeling strength (also S3)
- Impact: High
- Explanation: One discriminated
ConsumerActionunion and oneapply()are the entire cross-layer command surface; the save format shares the same seam; lagged knowledge is encoded in the types (e.g.alivevsknownDead), and the Ascension verdict is decided at send time but revealed at return time. - Evidence:
src/sim/scenario.ts:78-94(ConsumerAction),:306-313validate-before-log,:249-261(CastStatus),:529-544(feed verdict);src/state/save.ts:13-27;src/state/simStore.ts:68-85(AscensionMirrordeliberately withholds the verdict) - Found by: Structure & Boundaries, Coupling & Dependencies, Error Handling & Observability
- Category: S1 Clear modular boundaries
- Impact: High
- Explanation: Grep-verified zero React imports in
src/sim/and zero ui imports insrc/state/; the sole exception is a test file (F28). Authored content is quarantined insim/data/; three.js scenes are lazy-loaded so the boundary is also a bundle boundary. - Evidence:
src/App.tsx:17-19(lazy scene imports); full-tree grep ofsrc/sim/for react/state/ui imports: none - Found by: Structure & Boundaries
- Category: S4 Dependency direction is stable (also S6)
- Impact: High
- Explanation: Zero
scenario.apply/scenario.advancecalls or state assignments anywhere insrc/ui/; all 16 action types route through store wrappers, and every simapplycase throws before its first mutation, so a save can never contain an unrewalkable action. - Evidence:
src/sim/scenario.ts:306-313, singleeventLog.pushat:712; grep ofsrc/ui/forscenario.apply: zero hits - Found by: Coupling & Dependencies, Integration & Data
- Category: S3 Loose coupling
- Impact: High
- Explanation: Finding modules never import
scenario.tsback —orchestrator.tsanddeeptime.tshave zero imports,curator.tsimports geometry/data only — so every feature module stays headlessly testable and no scenario↔feature cycle exists. - Evidence:
src/sim/orchestrator.ts(zero imports),src/sim/deeptime.ts(zero imports),src/sim/curator.tsimports (bodies/constants/evidence/ephemeris/vec3); no circular imports found in the sim graph - Found by: Coupling & Dependencies
- Category: S12 Resilience patterns
- Impact: High
- Explanation:
serialize()emits only{version, epoch, seed, ConsumerDeltas}— never mutable derived state — andapply()is synchronous with autosave running between frames, so no instant exists at which a half-applied mutation can be captured. - Evidence:
src/state/save.ts:20-27(serialize);src/sim/scenario.ts:718-755(idempotentsettle()),:296-300(negative-dt guard),:1218-1220(non-decreasing rewalk guard) - Found by: Integration & Data
- Category: S11 Testability & coverage (also S8)
- Impact: High
- Explanation: 154 tests across 15 files run the whole walk headless in under a second, including full endng walk;
stateHash()is asserted in 13 places to prove bit-for-bit rewalk equivalence, giving the determinism pillar a direct cheap oracle. Sim layer sits at ~92% statement coverage. - Evidence:
src/sim/travel.test.ts:92-106(rewalk hash + mid-flight save round-trip),src/sim/scenario.ts:1180-1199(stateHash) - Found by: Security & Code Quality, Error Handling & Observability
- Category: S2 High cohesion
- Impact: High
- Explanation: Each mechanic is a small pure module with a colocated test and a header docstring naming its ADR and determinism argument.
- Evidence:
src/sim/chorus.ts(102 lines),credibility.ts(67),curator.ts(347, single purecuratorView),deeptime.ts(44),orchestrator.ts(58) - Found by: Structure & Boundaries
- Category: S6/S9/S12
- Impact: Medium-High
- Explanation:
Math.random/Date.nowappear only in prohibition comments; transmitter lifetimes are a closed-form FNV hash of (seed, bodyId) — savescum-proof and immune to call-count effects; thePrngfield is never even consumed, so no call-order randomness exists. - Evidence:
src/sim/scenario.ts:922-929(transmitterLifetimeS),:265andsrc/sim/prng.ts:5(prohibitions) - Found by: Integration & Data, Error Handling & Observability, Security & Code Quality
- Category: S6 (also S11)
- Impact: Medium
- Explanation: Raw JPL Horizons JSON is committed as provenance; the generated file carries source/fetch-date/units headers; ephemeris tests assert against Horizons vectors at three epochs — external ground truth, not self-referential snapshots. (Caveats: F21, F30.)
- Evidence:
data-raw/,src/sim/data/elements.gen.ts:1-10(provenance header),scripts/bake-elements.mjs:26-31(field/unit validation),src/sim/ephemeris.test.ts - Found by: Integration & Data, Security & Code Quality
- Category: S7 Robust error handling
- Impact: Medium
- Explanation: The sim throws walk-readable sentences captured into 7 per-domain error slots rendered beside the causing control;
introductionShortfall/eraseShortfallreturn the same sentences the sim throws, so button hints and rejections share one source. (Caveat: F10.) - Evidence:
src/state/simStore.ts:154-165(error slots),src/sim/orchestrator.ts:42-58 - Found by: Error Handling & Observability
- Category: S7 (also S8)
- Impact: Medium
- Explanation: Negative dt, non-monotonic delta logs, unknown save versions, invalid gamma/distances/ids all throw with labeled messages rather than propagating NaN; load never crashes the app. (Major caveat: the load-path diagnostics are then swallowed — F3.)
- Evidence:
src/sim/scenario.ts:296-300,:1217-1220("corrupt save: delta log epochs must be non-decreasing");src/state/save.ts:45;src/sim/travel.ts:50,ephemeris.ts:32,40,evidence.ts:149-151 - Found by: Error Handling & Observability, Security & Code Quality
- Category: S10 Security built-in
- Impact: Medium
- Explanation: Zero
dangerouslySetInnerHTML/innerHTML/eval/new Function/fetch/XMLHttpRequest/WebSocketinsrc/; CLAUDE.md's "no runtime network calls" claim verified true in code. - Evidence: full-tree greps, all zero hits;
index.htmlloads only the local module script - Found by: Security & Code Quality
- Category: S14 Simple, pragmatic abstractions
- Impact: Medium
- Explanation: 70-line versioned save module, 54-line loop with background-tab clamp, FNV-1a hash chosen for cheapness, one enforcement point for panel exclusivity; approximations are stated where made with revisit conditions. No single-implementation interfaces, factories, or DI machinery anywhere.
- Evidence:
src/state/simLoop.ts:23-25(tab clamp),src/state/simStore.ts:203-214(PANELS_CLOSED),src/sim/messages.ts:9-16andconstants.ts:31-35(documented approximations) - Found by: Structure & Boundaries, Coupling & Dependencies
- Category: S5 Dependency management hygiene
- Impact: Medium
- Explanation: Six runtime dependencies, no utility-library bloat, no runtime data-fetching; three.js weight lazy-loaded off the initial path. (Caveat: one of the six is unused — F26.)
- Evidence:
package.jsondependencies;src/App.tsx:17-19 - Found by: Coupling & Dependencies
- Category: S9 Configuration discipline
- Impact: Medium
- Explanation: Physical constants carry citations (IAU 2012 AU, DE440 GM_sun); UI panels import
CHORUS_MIN_GAMMA,DEEP_TIME_MIN_GAMMA,ORCHESTRATOR_*etc. from sim modules; tuning surfaces are labeled provisional. (Caveat: formulas and epsilons are still copied — F11/F12/F31.) - Evidence:
src/sim/constants.ts;src/ui/Hud.tsx:12-30,EvidencePanel.tsx:8-12,ConcordatPanel.tsx:10-15;src/sim/scenario.ts:226-236(ENDSCENARIO_*) - Found by: Error Handling & Observability
- Category: S12 Resilience patterns
- Impact: Medium (capped by F2 — the version covers the envelope only)
- Explanation: Version field, migration point, and defensive load existed from the first save commit — unusually disciplined scaffolding. The recovery policy is flawed (F3), but the scaffolding is right.
- Evidence:
src/state/save.ts:11,39-47,58-66; present since commit 4e4a8ee - Found by: Integration & Data
- Category: S11
- Impact: Low
- Explanation:
make coverruns vitest coverage; thecoverage/output directory is properly gitignored, not committed cruft. - Evidence:
Makefilecover:target;@vitest/coverage-v8in devDependencies - Found by: Security & Code Quality
[F1] Finding resolution order depends on tick size — rewalk/skip can produce a different finding than live walk
- Category: 19 Lack of idempotency (determinism-invariant violation)
- Impact: High
- Explanation:
settle()resolves concurrent findings in fixed code order (deep-time block before ascension-feed block), each guarded first-finding-wins — not in epoch order as ADR 0003 promises. A pending ascension feed (resolves in hours) and a lit deep-time leap (arrives years later) can legally coexist; fine-tick live walk crosses the feed first →ascensionfinding, while loading a save past the flare (or pressing "Skip to the flare", a single epoch jump) runs the deep-time block first →deep-timefinding, with divergent finding id, epoch,tauOffset, and conveyor state. - Evidence:
src/sim/scenario.ts:716-756(settle(), deepTime at:736-745before feed at:749-755);:603-637(deeptime.dilatenever checks a pending feed);src/state/save.ts:29-37(deserializejumps epoch thenadvance(0));src/state/simStore.ts:524-533(skipToFlare) - Found by: Integration & Data; verifier CONFIRMED by walking the scenario end-to-end
[F2] Save version covers only the envelope; any rebalance silently invalidates and destroys old saves
- Category: 24 Inconsistent API contracts
- Impact: High
- Explanation: Rewalk re-validates every logged action against current tuning and content (driver checks,
availableFrom, inbox membership, gamma minima), so the real save contract includes every tuning constant — yetSAVE_VERSION = 1has never been bumped while the action union grew from 8 to 16 types. CLAUDE.md blesses rebalancing as "a data change", but any such change can make rewalk throw → load returns null → fresh scenario → autosave overwrites the old save within 30 s. - Evidence:
src/state/save.ts:11(SAVE_VERSION = 1);src/sim/scenario.ts:345,570,624(driver),:456(availableFrom),:381(inbox membership);src/state/simLoop.ts:37-40(autosave) - Found by: Integration & Data
- Category: 26 Poor transactional boundaries (also 20)
- Impact: High
- Explanation:
loadFromStorage's blanketcatch { return null }swallows the deliberately good downstream diagnostics ("unknown save version", "corrupt save: …epochs must be non-decreasing"); nothing is quarantined, backed up, or logged, and the 30 s autosave clobbers the possibly recoverable save under the same key. Downgrading after a future version bump destroys the save by design. - Evidence:
src/state/save.ts:58-66, excerpt:catch { return null; };src/state/simStore.ts:18;src/sim/scenario.ts:1219 - Found by: Integration & Data, Error Handling & Observability
- Category: 32 Missing test coverage for critical paths
- Impact: High
- Explanation: All 15 test files live in
src/sim/; nothing importssimStore.ts(619 lines) orsimLoop.ts, yet simStore contains real logic — skip sequences driving the sim viadebug.setEpoch, arrival detection, error capture, the module boot path. This is exactly the layer where F1's skip trigger and F3's load path live.save.ts:45-69(localStorage/migration edges) is also uncovered. - Evidence:
src/state/simStore.ts:448-457,524-564(skip sequences),:221-235(arrivalFrom); coverage run: 154 tests, state layer absent from report - Found by: Security & Code Quality
- Category: 21 No observability plan
- Impact: Medium
- Explanation: No error boundary, no
window.onerror/unhandledrejection, zeroconsole.*in production code; render-path non-null assertions can white-screen the app with no message. A refresh recovers via the autosave boot path, so the cost is a blank page, up to 30 s of lost progress, and zero diagnostics. - Evidence:
src/main.tsx/src/App.tsx(nothing);src/ui/Hud.tsx:670(useSimStore((s) => s.transit)!),:548;src/ui/ConcordatPanel.tsx:53(joinedEpoch!) - Found by: Error Handling & Observability
- Category: 13 Inconsistent boundaries (also 6, 17)
- Impact: Medium
- Explanation: The store's own docs promise a "thin Zustand mirror", and
AscensionMirrordeliberately withholds the feed verdict — yetFindingOverlayreadsscenario.state.ascension?.feed?.flawsdirectly, and five other components read live scenario state in render, relying on no-op subscriptions (useSimStore((s) => s.epoch);) to force re-renders — tearing-prone under React 19 concurrent rendering. Writes remain fully disciplined; the erosion is read-side only. - Evidence:
src/ui/Inbox.tsx:28,33;Hud.tsx:41,605(planTrip(..., scenario.state.conveyor.aKmS2));ConcordatPanel.tsx:85,90;EvidencePanel.tsx:34,184,186;FindingOverlay.tsx:72;NavMap.tsx:152;src/state/simStore.ts:68-85 - Found by: Structure & Boundaries, Coupling & Dependencies, Integration & Data, Error Handling & Observability (4-specialist agreement)
[F7] Shotgun surgery: every finding/mechanic touches 6–10 files across three parallel dispatch sites
- Category: 9 Shotgun surgery
- Impact: Medium
- Explanation: Each feature must be threaded through the action union, the lockout set, an
apply()case,settle(),nextEventEpoch(), a mirror field, a hand-written wrapper, a dedicated error slot, an finding card, and a help section. Git confirms the pattern is mechanical: the last three finding commits touched 8, 8, and 10 files respectively. - Evidence:
src/sim/scenario.ts:78-94,189-203,331-711,716-756,1067-1113;src/state/simStore.ts:154-165,385-618; commits b069937, cb11ae6, ec33a6c (git show --stat) - Found by: Structure & Boundaries
- Category: 2 God object (also 4)
- Impact: Medium
- Explanation: 1225 lines importing 21 sim modules + 5 data modules, owning the action union, apply/advance/settle, and every derived query — highest fan-in × fan-out in the repo, modified by every feature commit. Mitigated by real delegation to pure subsystems, but
apply()alone is a ~380-line 16-way switch and the file grows monotonically per feature. - Evidence:
src/sim/scenario.ts:263-1200;transmitterLifetimeS(:922-929) duplicates the FNV-1a loop instateHash(:1193-1197); direct test filescenario.test.tsis 57 lines - Found by: Structure & Boundaries, Coupling & Dependencies
- Category: 3 Tight coupling (chatty-recomputation analog of 15)
- Impact: Medium
- Explanation:
syncFromSim()runs unconditionally each animation frame andmirror()recomputesunreadCount()(full inbox: every letter × lag × scenarioline walk),nextEventEpoch(),curator(),cast(),chorusTranscripts(),credibility()— all returning fresh references, so subscribed panels re-render at display refresh rate regardless of change. Inbox then computesscenario.inbox()again in render. - Evidence:
src/state/simLoop.ts:36;src/state/simStore.ts:237-318(mirror());src/ui/Inbox.tsx:33 - Found by: Coupling & Dependencies, Integration & Data
- Category: 34 Inconsistent error conventions (also 20)
- Impact: Medium
- Explanation: 12+ actions wrap
scenario.applyin try/catch with a diegetic error slot, butmarkRead,collectEvidence, andredrivercall it bare; the sim throws for all three, so a stale-mirror click throws uncaught in a React event handler — action lost, nothing surfaced. - Evidence:
src/state/simStore.ts:566-570,572-576,590-594;src/sim/scenario.ts:382,445-461,354-357 - Found by: Error Handling & Observability
- Category: 25 Business logic in the UI
- Impact: Medium
- Explanation:
Hud.tsxre-derivesconst cost = gamma - 1;instead of callingdeepTimeDriver()— which it could import from a module it already imports from. A rebalance of the blessed tuning surface silently desyncs the planner's cost display and afford-check from whatscenario.applycharges. - Evidence:
src/ui/Hud.tsx:494vssrc/sim/deeptime.ts:37-39(return gamma - 1;), charged atsrc/sim/scenario.ts:623 - Found by: Error Handling & Observability
- Category: 25 Business logic in the UI
- Impact: Medium
- Explanation: The HUD forks the comparison-and-epsilon logic of
introductionShortfall(), whose own docstring designates it as the shared source ("scenario.ts throws these and the UI can show them"). Constants are shared; the rule logic is not. - Evidence:
src/ui/Hud.tsx:361-363(listenedYears >= AMBASSADOR_COORD_YEARS - 1e-9…) vssrc/sim/orchestrator.ts:42-58 - Found by: Error Handling & Observability
[F13] stateHash() omits finding (and transmitters/purges), so hash equality does not certify finding equality
- Category: 19/24 Verification gap
- Impact: Medium
- Explanation: The hash covers
[epoch, seed, conveyor, readMessageIds, evidence, ascension, eventLog]. Transmitters/purges are pure folds over the hashed eventLog (redundant-safe), butfindingis the one settle-derived field — exactly the F1 divergence class — and no test compares fine-tick live vs loaded scenarios at all. - Evidence:
src/sim/scenario.ts:1183-1191; only scenario-level rewalk testscenario.test.ts:36-51usesdebug.setEpochalone - Found by: Integration & Data
- Category: 21 No observability plan
- Impact: Medium
- Explanation: The save file stores no hash and
deserializenever verifies the rewalked scenario against anything; a determinism regression that slips past the suite corrupts saves invisibly at every load, with no dev-mode assertion either. - Evidence:
src/sim/scenario.ts:1180(doc: "save/rewalk verification");src/state/save.ts:13-18(SaveFilehas no hash field) - Found by: Error Handling & Observability
- Category: 24 (also 30 — the app's only trust boundary)
- Impact: Medium
- Explanation: Only
versionis inspected;seed/epoch/consumerDeltasshapes are never validated, so a string epoch survivesadvance(0)into a loaded-but-NaN scenario instead of a clean reset. Severity bounded by the boundary being the consumers's own localStorage. - Evidence:
src/state/save.ts:62, excerpt:deserialize(JSON.parse(rawJson) as SaveFile) - Found by: Integration & Data, Security & Code Quality
- Category: 21
- Impact: Medium
- Explanation:
frame()has no try/catch, so an exception prevents the trailingrequestAnimationFrame;rafIdkeeps its stale value, sostartSimLoop()'s re-entry guard blocks any restart. Clocks freeze, autosave stops, no signal. - Evidence:
src/state/simLoop.ts:20-42, guard at:45(if (rafId !== null) return) - Found by: Error Handling & Observability
- Category: 27 Temporal coupling
- Impact: Medium
- Explanation: Every action wrapper repeats
scenario.apply(...); saveToStorage(scenario); set({...mirror()})with nothing enforcing order or completeness. Separately,skipToFlareandskipToSessionEndjump the epoch without routing througharrivalFrom(), safe only via an unstated transit/session/leap mutual-exclusion invariant. - Evidence:
src/state/simStore.ts:385-618(wrappers),:221(arrivalFrom),:448,524 - Found by: Coupling & Dependencies
- Category: 11 Low cohesion
- Impact: Medium
- Explanation: Hud.tsx (708 lines) is "every eventside interaction in the scenario" —
BodyPanelis a hardcoded ladder of 7 conditional feature sections, growing +151/+65 lines per finding commit.SimUiStateinterleaves ~50 fields of sim mirror data, panel chrome, badge bookkeeping, error slots, and ~30 actions. - Evidence:
src/ui/Hud.tsx:133-141(BodyPanel);src/state/simStore.ts:100-201 - Found by: Structure & Boundaries
[F19] Scenario constructed and localStorage rewalked at module import time; const binding; hardcoded seed
- Category: 1 Global mutable state (also 12, 27)
- Impact: Medium
- Explanation: Importing simStore touches localStorage and rewalks the full action log as a side effect; the
constbinding means "new walk" is onlyclearStorage()+window.location.reload(); the seed is hardcoded so "pure function of (epoch, seed, log)" has exactly one seed in practice. This import side effect is a direct cause of F4's untestability. - Evidence:
src/state/simStore.ts:18, excerpt:export const scenario: SimScenario = loadFromStorage() ?? new SimScenario(0xc0ffee);;src/ui/HelpPanel.tsx:111-114;src/state/simLoop.ts:13-16 - Found by: Structure & Boundaries, Coupling & Dependencies, Error Handling & Observability (3-specialist agreement)
[F20] Save writes fail fully silently while the eventLog grows without bound toward the quota that makes them fail
- Category: 26 (also 20)
- Impact: Medium
- Explanation: The empty catch on
saveToStorageemits nothing, not even a console line — Safari private mode means hours of walk with zero persistence and zero indication. Every skip/scrub appends adebug.setEpochdelta, so the save grows monotonically toward the ~5 MB quota, after which every save fails silently forever. - Evidence:
src/state/save.ts:49-56(emptycatch {});src/state/simStore.ts:354-364,448-457,524-564 - Found by: Integration & Data, Error Handling & Observability
[F21] Pluto barycenter branch and orbitalPeriodS untested despite CLAUDE.md's ephemeris-testing claim
- Category: 32 (steering-file contradiction)
- Impact: Medium
- Explanation: CLAUDE.md says moons/dwarfs/Pluto are tested against Horizons reference vectors, but
ephemeris.test.tscontains zero pluto/charon assertions and coverage confirms the branch never runs under test;orbitalPeriodS(UI orbit drawing depends on it) is also untested. - Evidence:
src/sim/ephemeris.ts:55-57(Pluto = −Charon ×CHARON_BARY_FACTOR),:78-86; ephemeris.ts at 66.7% statements - Found by: Security & Code Quality
- Category: 25
- Impact: Low
- Explanation: The panel computes
canPlaceHerefromaliveTransmitters()(excludes expired caches) while the sim blocks on all ever-placed non-purged caches — so a discovered-but-not-dismantled cache vanishes from the list, the button enables, and the click yields "a transmitter is already hidden at this body" for an invisible cache. The throw is caught and surfaced, so no crash. - Evidence:
src/ui/EvidencePanel.tsx:211-212vssrc/sim/scenario.ts:393-399; alive filter at:951-960 - Found by: Error Handling & Observability
- Category: 28 Magic strings
- Impact: Low
- Explanation:
"chorus-06"gates the Orchestrator section render and"chorus-05"gates refit + Time, both matching data rows with no named constants. The sim-side literal is indirectly test-covered; the uncovered risk is the Hud copy, where a data rename silently hides an finding's UI. - Evidence:
src/ui/Hud.tsx:357;src/sim/scenario.ts:1006(knowsFlareDate);src/sim/data/chorus.ts:102,112 - Found by: Error Handling & Observability
- Category: 9/28
- Impact: Low
- Explanation: The
distance(...)/C_KM_Sidiom is copy-implemented per site; the ConcordatPanel copy must stay numerically identical to the sim's feed math or its "her reply, at the earliest" preview diverges from the actualresolveEpoch. - Evidence:
src/sim/scenario.ts:531-535,841-845,1123-1124;src/sim/curator.ts:122-126;src/ui/ConcordatPanel.tsx:87-91,101(reply = 2 * lagS) - Found by: Structure & Boundaries, Error Handling & Observability
- Category: 6 Leaky abstraction
- Impact: Low
- Explanation:
deserialize()assignsscenario.state.epochfrom outside the sim then callsadvance(0)to settle;readonly stateguards only reassignment, and no sim API exists for "restore to epoch without logging". Single documented site, but an encapsulation hole other callers could copy (its behavioral consequence is F1). - Evidence:
src/state/save.ts:34, excerpt:scenario.state.epoch = migrated.epoch; - Found by: Coupling & Dependencies
- Category: 31 Unused dependencies
- Impact: Low
- Explanation: Zero imports anywhere in
src/; ADR 0016 confirms the dreiStarsusage was replaced. Tree-shaken from the bundle, so the cost is install weight and audit surface. - Evidence:
package.json("@react-three/drei": "^10.7.7");docs/adr/0016-aberration-doppler-shaders.md:23 - Found by: Coupling & Dependencies, Security & Code Quality
- Category: 31
- Impact: Low
- Explanation: No config, no e2e directory, no script or Makefile target references it — one of the heaviest possible dev deps (downloads browser binaries), likely used ad hoc during autonomous development and never wired in.
- Evidence:
package.jsondevDependencies ("playwright": "^1.61.1") - Found by: Coupling & Dependencies, Security & Code Quality
- Category: 13 (steering-file contradiction)
- Impact: Low
- Explanation: CLAUDE.md's strict
sim → state → uidirection and its all-tests-in-src/sim/convention collide: save round-trip tests can only live in sim by importing state. Production sim code is clean; this is the sole violation. - Evidence:
src/sim/travel.test.ts:6, excerpt:import { deserialize, serialize } from "../state/save"; - Found by: Structure & Boundaries, Coupling & Dependencies, Security & Code Quality (3-specialist agreement)
- Category: 32
- Impact: Low
- Explanation: Expected for an r3f walk overall, but
format.ts(imported in 9 places) andprojection.tsare pure functions — free test wins left on the table. - Evidence: all 15 test files under
src/sim/;src/ui/format.ts,src/ui/projection.ts - Found by: Security & Code Quality
- Category: 24 (pipeline contract)
- Impact: Low
- Explanation: The bake is a manual step and both artifacts are committed; nothing asserts
bake(data-raw) === elements.gen.ts, and moon tolerances (up to ~15°/6%) could let a stale gen file pass the indirect ephemeris tests. Mitigated by provenance headers and bake-time validation. - Evidence:
scripts/bake-elements.mjs:1-9("Run manually after refreshing data"); no CI/test rebake-and-diff - Found by: Integration & Data
- Category: 28
- Impact: Low
- Explanation: The same undocumented tolerance literal appears in driver/threshold checks on both sides of the sim/UI boundary; the UI copies must match the sim's by hand for buttons to agree with
apply(). - Evidence:
src/sim/scenario.ts:345,570,624;src/ui/Hud.tsx:85,282,361-362,495,608;src/sim/chorus.ts:91;src/sim/orchestrator.ts:47,52 - Found by: Error Handling & Observability
- Category: 25/28
- Impact: Low
- Explanation: The racetrack gamma ceiling (
refit ? 40 : 7) and Time slider max (100) exist only as widget attributes; the sim validates only minimums plus driver, so the effective ceilings are business rules living as unnamed literals in a component. - Evidence:
src/ui/Hud.tsx:293,510;src/sim/scenario.ts:556-572,618-625 - Found by: Error Handling & Observability
- Category: 31
- Impact: Low
- Explanation: Pure clutter; one 0-byte git lock file, nothing references it.
- Evidence:
_to_delete/HEAD.lock(0 bytes, untracked) - Found by: Security & Code Quality
| # | Type | Status | Finding |
|---|---|---|---|
| 1 | Global mutable state | Observed | #F19 |
| 2 | God object | Observed | #F8 |
| 3 | Tight coupling | Observed | #F9 |
| 4 | High/unstable dependencies | Observed | #F8, #F26 |
| 5 | Circular dependencies | Not observed (sim graph verified as a DAG) | — |
| 6 | Leaky abstractions | Observed | #F6, #F25 |
| 7 | Over-abstraction | Not observed | — |
| 8 | Premature optimization | Not observed | — |
| 9 | Shotgun surgery | Observed | #F7, #F24 |
| 10 | Feature envy / anemic domain model | Not observed (plain-data state + pure functions is required by the rewalk/hash design) | — |
| 11 | Low cohesion | Observed | #F18 |
| 12 | Hidden side effects | Observed | #F19 |
| 13 | Inconsistent boundaries | Observed | #F6, #F28 |
| 14 | Distributed monolith | Not applicable (single-process SPA) | — |
| 15 | Chatty service calls | Not applicable (nearest analog: #F9) | — |
| 16 | Synchronous-only integration | Not applicable | — |
| 17 | No clear ownership of data | Observed (read-side only) | #F6 |
| 18 | Shared database across services | Not applicable (one localStorage key, one writer) | — |
| 19 | Lack of idempotency | Observed | #F1, #F13 |
| 20 | Weak error handling strategy | Observed | #F3, #F10, #F20 |
| 21 | No observability plan | Observed | #F5, #F14, #F16 |
| 22 | Configuration sprawl | Not observed (countered by S15) | — |
| 23 | Dependency injection misuse | Not observed | — |
| 24 | Inconsistent API contracts | Observed | #F2, #F15, #F30 |
| 25 | Business logic in the UI | Observed | #F11, #F12, #F22, #F32 |
| 26 | Poor transactional boundaries | Observed | #F3, #F20 |
| 27 | Temporal coupling | Observed | #F17, #F19 |
| 28 | Magic numbers/strings everywhere | Observed | #F23, #F31 |
| 29 | "Utility" dumping ground | Not observed (format.ts, vec3.ts, constants.ts all small and single-purpose) | — |
| 30 | Security as an afterthought | Observed (minor; trust boundary is consumer's own localStorage) | #F15 |
| 31 | Dead code / unused dependencies | Observed | #F26, #F27, #F33 |
| 32 | Missing test coverage for critical paths | Observed | #F4, #F21, #F29 |
| 33 | Hard-coded credentials or secrets | Not observed (full grep; only narrative prose hits) | — |
| 34 | Inconsistent error/logging conventions | Observed | #F10 |
| # | Category | Status | Finding |
|---|---|---|---|
| S1 | Clear modular boundaries | Observed | #S2 |
| S2 | High cohesion | Observed | #S7 |
| S3 | Loose coupling | Observed | #S1, #S4 |
| S4 | Dependency direction is stable | Observed | #S3 |
| S5 | Dependency management hygiene | Observed | #S14 |
| S6 | Consistent API contracts | Observed | #S1, #S8, #S9 |
| S7 | Robust error handling | Observed | #S10, #S11 |
| S8 | Observability present | Observed (determinism tests as the observability plan; runtime side weak — see F5/F14) | #S6, #S11 |
| S9 | Configuration discipline | Observed | #S15 |
| S10 | Security built-in | Observed | #S12 |
| S11 | Testability & coverage | Observed (sim layer; state/ui gaps in F4/F29) | #S6, #S9, #S17 |
| S12 | Resilience patterns | Observed | #S5, #S16 |
| S13 | Domain modeling strength | Observed | #S1 |
| S14 | Simple, pragmatic abstractions | Observed | #S13 |
src/state/— the highest-risk, least-tested code in the repo: zero test coverage (F4) over the save/load data-loss paths (F2, F3, F15, F20), the import-time boot side effect (F19), and the skip logic that triggers F1.src/sim/scenario.ts— the hub everything routes through (F8):settle()'s code-order finding resolution (F1), thestateHashblind spot onfinding(F13), and the per-feature growth that drives shotgun surgery (F7).src/ui/Hud.tsx(with EvidencePanel/ConcordatPanel) — the accretion point for forked sim rules (F11, F12, F22, F24, F32), mirror-bypass reads (F6), and per-finding growth (F18).
- Is the F1 scenario (pending ascension feed + lit time drive) reachable in a normal walkthrough, and is the intended contract that
settle()resolve events in epoch order per ADR 0003 — or should the ADR document a first-finding-wins code order? - What is the intended save-compatibility policy across rebalances, given CLAUDE.md declares them "a data change" while rewalk re-validates every logged action against current tuning (F2)?
- Is the Zustand mirror meant to be the sole read contract for the UI, or is direct read-only access to the live
scenariosanctioned — and if sanctioned, where should that be documented soAscensionMirror-style information hiding isn't silently bypassed (F6)? - Which state-layer behaviors (skip sequences,
arrivalFrom, load/migrate/quota paths) count as critical paths that need tests before more content lands (F4)? - Was Pluto's barycenter offset intended to be covered by the Horizons reference-vector tests, per CLAUDE.md's claim (F21)?
- Agents dispatched: 5 specialists (Structure & Boundaries; Coupling & Dependencies; Integration & Data; Error Handling & Observability; Security & Code Quality) + 1 verifier
- Scope: full repository (~65 source files, ~10.5k lines) at commit 06a5cf9
- Raw findings: 79 (51 flaws, 28 strengths)
- Verified findings: 50 (33 flaws, 17 strengths)
- Filtered out: 29 (18 cross-specialist duplicates merged, 2 dropped as false positives/subsumed, 1 folded into another finding, plus merge consolidation)
- By impact (flaws): 4 High, 17 Medium, 12 Low
- By impact (strengths): 7 High, 9 Medium, 1 Low
- Steering files consulted: CLAUDE.md, docs/design.md, docs/adr/0001–0016