Use this prompt for each phase/sub-phase. Ralph should read the relevant files first, then implement, then run tests, confirming before moving on.
REFACTOR-PLAN.md before starting each phase.md cross-references if mentionedbun test after each sub-phase to confirm nothing breaksschema/types.tsLook at: _parse.ts (top types), _parse.ts (cleanId function), _stats.ts (uses of RoomType, Character), real run files in __tests__/fixtures/
Deliver: src/pages/tools/sts2dash/schema/types.ts with:
RoomType union type — verify values from real files (monster, elite, boss, rest_site, shop, treasure, event, ancient, unknown)Character union type — STS2 has: IRONCLAD, SILENT, REGENT, NECROBINDER (not STS1’s WATCHER/DEFECT)ROOM_TYPE_MAP and CHARACTER_MAP records — built from actual run datacleanId() moved from _parse.tsr1() and fmtId() utility exportsVerify character map against real files: Run bun --eval to check players[0].character values in fixture files. Should be: CHARACTER.IRONCLAD, CHARACTER.SILENT, CHARACTER.REGENT, CHARACTER.NECROBINDER.
Test: No new tests needed yet — just verify existing code still works (bun test)
schema/v8.tsLook at: _parse.ts (parseRuns function body), existing test fixtures in __tests__/fixtures/ (schema 8 files: 1772794581.run, 1773155293.run, 1774008838.run)
Deliver: src/pages/tools/sts2dash/schema/v8.ts with:
SchemaV8 interface matching the actual STS2 .run JSON shape for schema_version 8isSchemaV8() type guardHint: Match the actual field names and nested shapes exactly as they appear in the raw JSON. Check a real schema 8 file with bun --eval to see all unique player_stats keys. Key fields: card_choices, cards_gained, cards_removed, upgraded_cards, ancient_choice, relic_choices, bought_relics, bought_potions, potion_used, rest_site_choices, current_hp, damage_taken, hp_healed, max_hp, gold_*.
schema/v9.tsLook at: _parse.ts (parseRuns function body), existing test fixtures in __tests__/fixtures/ (schema 9 files: 1774708464.run, 1774719367.run, 1775938543.run)
Deliver: src/pages/tools/sts2dash/schema/v9.ts with:
SchemaV9 interface matching the actual STS2 .run JSON shape for schema_version 9isSchemaV9() type guardHint: Compare schema 9 vs 8 — check which fields differ. Key differences: cards_removed items now have floor_added_to_deck; cards_transformed added; relics_removed added; bought_potions replaced by potion_choices.
schema/index.tsLook at: _parse.ts (how it handles missing schema_version)
Deliver: src/pages/tools/sts2dash/schema/index.ts with:
detectSchemaVersion(data) functionisValidRun(data) functionnormalize(data) function (initially just returns data as AnySchema)__tests__/schema.test.ts using real fixturesLook at: __tests__/fixtures.test.ts, __tests__/fixtures/ (real .run files)
Deliver: __tests__/schema.test.ts with tests for:
isSchemaV8() returns true for schema 8 files, false for schema 9isSchemaV9() returns true for schema 9 files, false for schema 8detectSchemaVersion() works for both versionsisValidRun() works for both versionsLoad fixtures using: JSON.parse(readFileSync(join(__dirname, 'fixtures', filename), 'utf-8')) — do NOT use the old createRun() helper pattern.
bun test. All schema tests pass. Then ask before proceeding.Run Interfaceschema/run.tsLook at: ParsedRun and RunDetail in _parse.ts, normalizeRoomType function, real run files
Deliver: src/pages/tools/sts2dash/schema/run.ts with:
Floor, CardChoice, AncientChoiceDetail, CardEnchanted sub-interfacesCardInDeck interface with id, upgrades (from current_upgrade_level), enchantment (from enchantment.id)Run interface — the union of all fields from both ParsedRun and RunDetailroom_type values use rest_site (not rest) — verified from real run filesdeck_counts, relic_stats, deaths) — those go in Phase 4Hint: The Run interface should contain EVERY field needed by _stats.ts and _app.ts. Check all usages of ParsedRun.* and RunDetail.* to make sure nothing is missed. The final deck cards have current_upgrade_level and enchantment in real v8/v9 files.
bun test. Then ask before proceeding._idb.tsLook at: existing _idb.ts, REFACTOR-PLAN.md Phase 3
Deliver: Rewritten _idb.ts with:
StoredRun interface ({ filename, savedAt, data })STORE_RUNS object store (drop STORE_DETAILS)saveRun(run: StoredRun), loadRun(filename), loadAllRuns()CacheData, saveCache, loadCache entirelysaveAllRunDetails, loadRunDetails, saveRunDetail, loadRunDetailWARNING: Bump DB_VERSION to 4. Add migration logic in onupgradeneeded to delete old object stores.
_idb.tsLook at: _app.ts (all IDB calls), index.astro (if any IDB calls)
Deliver: Update _app.ts to:
_runDetailCache, _runDetails staterawRunsData stateCacheData importsaveCache() calls with saveRun() callsloadCache() logic with loadAllRuns() logicloadRunDetail() calls with loadRun() callsHint: After this step, _app.ts should only reference Run objects, never raw JSON directly. The IDB layer returns StoredRun, which gets converted to Run by the parser.
bun test. Open the page in browser, verify it loads without errors. Ask before proceeding.stats/summary.tsLook at: _stats.ts → StatsSummary and the summary-computing portion of computeStats()
Deliver: src/pages/tools/sts2dash/stats/summary.ts with:
emptySummary constantcomputeSummary(runs: Run[]): StatsSummary — pure, no mutationRunstats/cards.tsLook at: _stats.ts → CardPickStat and card-stat-computing portion
Deliver: src/pages/tools/sts2dash/stats/cards.ts with:
computeCardStats(runs: Run[]): CardPickStat[] — pureLook at: _stats.ts → remaining stat computations
Deliver: Each in its own file:
stats/relics.ts → computeRelicStats()stats/deaths.ts → computeDeathStats()stats/winrate.ts → computeWinRateOverTime()stats/restsite.ts → computeRestSiteStats()stats/upgrades.ts → computeUpgradeStats()stats/ancient.ts → computeAncientStats()Each should be a pure function taking Run[], returning typed output.
stats/index.tsLook at: _stats.ts → StatsOutput interface
Deliver: src/pages/tools/sts2dash/stats/index.ts with:
StatsOutput interface matching current shape (consolidate from sub-modules if needed)computeStats(runs: Run[]): StatsOutput that calls all sub-functionsbun test. All stat function tests pass. Then ask before proceeding.Run Conversionschema/toRun.tsLook at: _parse.ts (parseRuns function body, parseRunDetailForCache function body), the new Run interface
Deliver: src/pages/tools/sts2dash/schema/toRun.ts with:
toRun(filename: string, data: AnySchema): Run — the ONE parsing function_parse.ts hereRunHint: This function should be the union of everything parseRuns + parseRunDetailForCache did. Read both carefully.
toRun testsLook at: __tests__/parse.test.ts, the JSON fixtures from Phase 1d
Deliver: __tests__/toRun.test.ts with tests covering:
map_point_history totalfloor_added_to_deck === 1)final_deck_cards populated with upgrades and enchantmentsdeck_size computed correctly at each floor (working backwards from final)_parse.tsLook at: every file that imports from _parse.ts
Deliver:
_parse.tsschema/toRun.ts, schema/run.ts, schema/types.ts_stats.ts imports if it was importing from _parse.tsbun test. All tests pass. Ask before proceeding.Look at: _app.ts (remaining state fields)
Deliver: Clean up _app.ts state — remove any fields that are no longer needed after Phase 3/4.
Look at: _app.ts → finishLoad(), loadFromCache(), renderAll()
Deliver: Verify the full flow works:
parseRuns() (was: dual objects)Run[] via toRun()saveRun()Run[] via toRun()computeStats(Run[])All should work without ParsedRun or RunDetail.
Run: bun test --all
Deliver: All tests pass. If any fail, investigate and fix.
ParsedRun and RunDetail types removed (not just unused)CacheData interface removed from codebaserawRunsData state removed from _app.ts_parse.ts deleted_stats.ts deleted (replaced by stats/)bun test)