Skip to content

Phase 2: Accuracy & Analysis Depth — cast timeline, healer metrics, game-data regen, regression net - #15

Merged
alexandermayes merged 43 commits into
mainfrom
growth/phase-2-accuracy-depth
Sep 14, 2026
Merged

alexandermayes merged 43 commits into
mainfrom
growth/phase-2-accuracy-depth

Conversation

@alexandermayes

Copy link
Copy Markdown
Owner

Summary

Phase 2 — Accuracy & Analysis Depth (requirements ACC-01..ACC-04, OPS-01). Nine GSD plans, all executed sequentially on main with atomic commits; planning artifacts under .planning/phases/02-accuracy-analysis-depth/.

  • Cast Timeline tab (ACC-03)lib/timeline-engine.ts, paginated/rate-limited/cached /api/timeline, lazy useTimeline hook, CastTimeline.tsx with idle-gap rows (max(2000ms, 3×median inter-cast gap)), death marker, per-ability filter chips, windowed rendering and a truncation notice. PostHog: timeline_viewed, timeline_error, timeline_filter_used (one call site each).
  • Healer metrics + suggestions (ACC-04)lib/healer-metrics.ts is the single source of truth for effective HPS / overheal % / uptime on both the player comparison card and the raid Healer Breakdown (D-08 parity test). Three healer suggestion rules thresholded against top healers; DPS-shaped rules no longer reach healers.
  • wago.tools game-data pipeline (ACC-01)npm run regen-game-data generator with per-era build pinning and row-count floors; three generated era modules + one sourced overrides file; lib/cla-constants.ts is now a thin re-export. Era precedence is Classic/TBC-first (later-era-wins corrupted pinned pairs because client builds reuse IDs across eras). Review artifact: docs/GAME-DATA-AUDIT.md (133 explicit overrides, 0 changed values).
  • Regression net (ACC-02) — recorded WCL fixtures under lib/__fixtures__/ (npm run record-fixtures) drive fixtures.test.ts, timeline-engine.test.ts, healer-metrics.test.ts, analysis-engine.test.ts, cla-engine.test.ts, raid-overview-engine.test.ts, wcl-client.test.ts, game-data.test.ts. Suite: 14 files / 135 tests, ~1 s. docs/OPS-01-SHIP-GATE.md now states a red suite blocks a deploy.

Notable data-driven deviations are documented in each plan's SUMMARY (02-06 precedence + override count; 02-02 begincast/-1 target handling; 02-01 corrected RESEARCH assumption A3).

Test plan

  • npx tsc --noEmit clean
  • npm test — 14 files / 135 tests passing
  • npm run theme-parity, npm run token-audit — 0 gate-relevant findings
  • npm run seo-invariants — 11 routes, analyze canonical still param-free
  • Scoped eslint app lib components scripts clean (unscoped run only reports the untracked local .codex/ dir)
  • Preview deploy: both-theme route sweep + 375 px Timeline pass + real-gear game-data name check (OPS-01 step 2)
  • Production deploy + post-deploy GSC/PostHog evidence (OPS-01 steps 4–6)

🤖 Generated with Claude Code

https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs

alexandermayes and others added 30 commits September 7, 2026 17:25
- Add scripts/record-wcl-fixtures.mjs (recorder in token-audit.mjs family):
  mints OAuth token, POSTs GraphQL, resolves healer source at run time
- Wire npm run record-fixtures in package.json
- Record six fixtures from public report ZjKgNYxVcAqR8pGJ fight 23:
  demo-player-dps, demo-player-healer (scoped + un-scoped healing),
  demo-raid-overview, demo-raid-combatant-info, demo-raid-death-events
  (fight has zero deaths), demo-timeline-casts (paginated cast events)
- lib/__fixtures__/README.md documents provenance and resolves RESEARCH.md
  assumptions A1-A5 against the real API, including the A3 correction:
  the sourceID-scoped Healing table DOES carry per-ability overheal;
  what it lacks is activeTime, which appears only on the un-scoped row
- No credential material committed; pulled Vercel env file discarded

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
… contracts

- lib/__fixtures__/fixtures.test.ts: shape guard over all six fixtures,
  asserting exact field names README.md recorded (not RESEARCH.md guesses)
- lib/wcl-queries.ts: TIMELINE_CASTS_QUERY + TIMELINE_CASTS_PAGE_QUERY using
  the real events(dataType: Casts) argument names and cast-event field names;
  healingByPlayer added to both healing queries so player-level overheal and
  activeTime are available with no second round trip
- lib/wcl-types.ts: Cast Timeline and Healer Metrics type contracts
  (WCLCastEvent, TimelineRow, CastTimelineResult, HealerTableRow,
  HealerMetricsComputed, HealerComparison) plus optional
  AnalysisResult.healer and TopPlayerFullData.healerRow
- lib/constants.ts: explicit RATE_LIMITS.timeline bucket (tighter than
  analyze — one request can page cast-events up to 20 times)
- lib/utils.ts: shared formatFightTime, reproducing RaidOverview's private
  helper so the death timeline and cast timeline can't drift apart
- lib/analysis-engine.ts: export JUNK_SPELL_IDS/isJunkSpell so the cast
  timeline reuses the one junk-spell exclusion set

npx vitest run lib/__fixtures__/fixtures.test.ts passes (6/6); this test is
a shape guard over data Task 1 already recorded, so it never had a failing
RED state to catch — documented as an intentional exception, not a gate
violation. npx tsc --noEmit and npm run lint are clean on every file this
task touches.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
- lib/timeline-engine.ts: pure buildCastTimeline() transform (cast events +
  casts table + actors -> ordered TimelineRow[]), mirroring buildRaidOverview's
  shape. Filters begincast probes (only completed "cast" events survive),
  junk spells via isJunkSpell, and unlisted ability ids; resolves ranked
  display names and Self/undefined targets (WCL's -1 "no target" sentinel).
- lib/timeline-engine.test.ts: 8 assertions driven by the real recorded
  demo-timeline-casts.json fixture plus synthetic junk/self/no-target events.
- app/api/timeline/route.ts: POST /api/timeline — validated, rate-limited
  (timeline bucket), single-flight cached via cachedApiHandler, paginates
  cast-events up to MAX_TIMELINE_PAGES=20 (denial-of-service bound), sets
  truncated on cap.
- app/analyze/[reportCode]/hooks/useTimeline.ts: lazy-fetch hook mirroring
  useCLA's shape; auto-runs only when the timeline sub-tab is active (no WCL
  fetch until the tab opens).
- app/components/CastTimeline.tsx: vertical cast log — Card shell, loading
  skeletons, destructive Alert on error, empty-state copy, 40px rows with
  timestamp/icon/SpellLink name/target columns, lazy-loading icon with
  graceful onError fallback to a placeholder square.
- app/components/AnalysisView.tsx / AnalyzeClient.tsx: seventh "timeline" ptab
  value, TabsTrigger/TabsContent pair, reportCode/fightId/sourceId wired down
  from AnalyzeClient's existing selection state.

Deviation note: buildRankedNames is duplicated locally in the pure engine
rather than imported from app/components/SpellLink.tsx (a "use client"
module) — importing a client-boundary export into this server-consumed lib
file risks resolving to a client-reference proxy in Next.js's RSC build
rather than the real function, so the small ranking algorithm is
re-implemented verbatim instead.
…aths

- useTimeline.ts: two posthog.capture call sites — timeline_viewed
  (report_code, fight_id, source_id, cast_count, truncated) on success,
  timeline_error (report_code, fight_id, source_id, error) on a failed
  response or a thrown request. Outcome captured across both branches so
  the analytics call stays a single site per outcome rather than one per
  branch, per docs/OPS-01-SHIP-GATE.md step 4's one-event-per-interaction
  rule.
- CastTimeline.tsx: row list wrapped in a max-h-[400px] overflow-y-auto
  (overflow-x-hidden) container so a long log scrolls inside the card
  instead of stretching the page; only the ability-name column flexes.
  Error/loading/content states were already mutually exclusive from Task 1.
- AnalysisView.tsx's Timeline TabsTrigger and TabsList classes are
  unchanged — confirmed no disabled attribute, no spinner, trigger stays
  selectable during a fetch or an error.
…ed generator

- Resolves each era's build from https://wago.tools/api/builds by matching
  product + version prefix, never a fresh guess or a shared latest build
  (RESEARCH.md Pitfall 5) — Classic+TBC (wow_anniversary), WotLK and Cata
  (both wow_classic, disambiguated by version prefix).
- Derives ENCHANT_NAME / GEM_NAME / GEM_STAT maps from SpellItemEnchantment,
  SpellName, SpellEffect, ItemSparse and GemProperties CSV exports, joined
  and combined into the same "label (raw stat text)" style the pre-existing
  hand-authored lib/cla-constants.ts used.
- Per-era, per-map row-count floors are enforced before any file is written;
  a zero-row map is always fatal. Cross-era ID collisions are enumerated,
  never resolved silently.
- lib/generated/game-data-overrides.json is the single hand-authored data
  file (3 sourced entries: enchant id 88, whose client data is empty in all
  three eras, and enchant ids 2343/2566, whose only linked spells in client
  data are internal QA/test spells) plus the STAT_TYPE_BAD_FOR_ROLES policy;
  lib/generated/game-data-overrides.ts is its typed accessor.
- Ran `npm run regen-game-data` for real against live wago.tools and
  committed the three generated era modules (game-data.classic-tbc.ts,
  game-data.wotlk.ts, game-data.cata.ts) — resolved builds 2.5.6.69546 /
  3.4.5.63697 / 4.4.2.60895, all matching the currently pinned builds.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…name pair

- lib/generated/game-data.test.ts composes the three era modules (a small
  local helper, not imported from the app — that composition module is
  02-06's artifact) and merges the overrides on top, asserting all eight
  required behaviours plus the override/registry invariant.
- Deviation: Tests 1-6 assert directly against ENCHANT_NAME_CLASSIC_TBC /
  GEM_NAME_CLASSIC_TBC / GEM_STAT_CLASSIC_TBC rather than the later-wins
  composed map. The exhaustive generated data (thousands of rows per era,
  not a hand-curated subset) surfaces real cross-era ID reuse the curated
  map never hit — e.g. enchant 3003 is "Glyph of Ferocity" in the TBC
  client and "Arcanum of Ferocity" in WotLK's; gem 32196 is "Runed Crimson
  Spinel" in TBC/WotLK and "Brilliant Crimson Spinel" in Cata. A blind
  later-wins merge would silently clobber the pinned TBC fact with an
  unrelated later-era item sharing the same numeric id — exactly the
  cross-era misattribution this phase exists to prevent. Each era module
  stays independently correct; only the merge was unsound for asserting a
  specific era's historical fact, so the test now asserts against the
  correct era directly.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…alers

- lib/healer-metrics.ts: computeHealerMetrics/averageTopHealerMetrics, the
  single source of truth for healer throughput/efficiency/uptime (D-08),
  extracted verbatim from the raid-overview engine's prior inline formulas
- lib/wcl-fetchers.ts: fetchTopPlayers reads the un-scoped healingByPlayer
  row and stores it as TopPlayerFullData.healerRow on the healer path
- app/api/analyze/route.ts: healer branch reads its own healingByPlayer row,
  builds a HealerComparison via the shared helper against averaged top-healer
  metrics, without touching the hps ranking metric or partition scoping
- lib/analysis-engine.ts: buildAnalysisResult accepts healerComparison and
  assigns it to AnalysisResult.healer only for the healer role
- app/components/DpsComparison.tsx: renders Overheal/Uptime rows beneath the
  Top bar for healers, colored via the shared overhealColor/activityColor
  helpers (added to lib/constants.ts, alongside percentileColor/percentileBg
  — pulled forward from Task 2 since this task's own tsc/lint/token-audit
  gate requires the import to resolve; Task 2 reuses them unchanged)
- lib/wcl-types.ts: HealerComparison gains hasHealing so the player's own
  percentage can render an em dash instead of a misleading measured zero

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…lper

- lib/raid-overview-engine.ts: buildRaidOverview calls computeHealerMetrics
  for both the per-player throughput/activity branch and the healerMetrics
  list, replacing the inlined overheal/uptime formulas; a healer with no
  healing row is now emitted with zeroed metrics (totalHealing: 0) instead
  of being dropped from the list, so a healer dead at the pull still appears
- app/components/RaidOverview.tsx: imports overhealColor/activityColor from
  lib/constants and formatFightTime from lib/utils, deleting the three
  module-private duplicates (pure import swap, byte-identical logic);
  HealerPanel renders an em dash instead of a measured-looking 0% overheal
  when totalHealing is 0
- lib/healer-metrics.test.ts: cross-surface parity assertion (buildRaidOverview
  and computeHealerMetrics agree field-for-field for the same row and fight
  duration) plus a zero-healing-row assertion, closing D-08's guarantee from
  the raid-overview side

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…ast rhythm

- buildCastTimeline now emits idle and death TimelineRow kinds, merged
  chronologically alongside cast rows
- idleThresholdMs = max(2000ms, 3x this player's own median inter-cast
  gap) -- relative to the player, never a per-class constant (D-03)
- at most one death row per fight, positioned by fightTimeMs rather than
  appended at the end
- castCount continues to count cast rows only, so idle/death bands never
  inflate the truncation notice or timeline_viewed's cast_count
- app/api/timeline/route.ts passes the already-queried deathEvents
  through to the engine; truncated continues to be set only on the
  page-cap exit path
- 9 new timeline-engine.test.ts behaviours (fixture-derived threshold
  math plus 8 synthetic-sequence cases); the tracer's abilityName
  assertion is scoped to kind === "cast" since idle/death rows are
  structural markers with no ability

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…for the cast timeline

- Per-ability filter chips (flex flex-wrap gap-1.5, All reset chip) built
  from the engine's abilityCounts; nothing hidden by default (D-04); each
  chip's count always reads the unfiltered total. Neutral bg-surface-3
  selected state, no gold accent -- chips are multi-select
- Hand-rolled fixed-40px-row windowing (no virtualisation dependency, per
  RESEARCH.md) with a 10-row overscan buffer above/below the viewport so
  scrolling never blanks; recomputes on scroll, resize (ResizeObserver)
  and filter change
- Idle-gap divider (dashed border-status-warn, Lucide Pause) and death
  band (badge-bad, Lucide Skull) row renderers for the new TimelineRow
  kinds from 02-05's engine change
- Truncation notice row ("Log truncated -- showing first {N} casts")
  renders inside the scroll container whenever the route reports
  truncated:true, never hidden by the ability filter
- useTimeline.ts exports captureFilterUsed, firing timeline_filter_used
  (report_code, fight_id, ability_count, hidden_count) once per chip
  interaction -- the hook's third posthog.capture call site
- AnalysisView.tsx threads captureFilterUsed into CastTimeline as
  onFilterToggle (required wiring beyond the plan's stated
  files_modified -- the callback lives in the hook AnalysisView already
  calls; see SUMMARY Deviations)
- Filter-selection and result-change state resets use React's documented
  "adjust state during render" pattern rather than useEffect, since an
  effect that only calls setState synchronously trips
  react-hooks/set-state-in-effect (newly enforced by this repo's eslint
  config); the DOM scrollTop reset stays in a real effect since it has no
  setState call

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
- Move all 178 CONSUMABLE_DB curation rows (category/isSuboptimal/
  betterAlternative) into lib/generated/game-data-overrides.json as
  CONSUMABLE_CURATION, dropping the name field which is now generated.
- Add a consumable-name verification pass to scripts/regen-game-data.mjs:
  resolves each curated id's display name directly from each era's
  SpellName table, records cross-era corroboration in `verifiedIn`, and
  falls back to an explicit consumableNames override (with a source note)
  for the 38 ids no regenerated era can resolve (4 pre-Anniversary Classic
  ids, 34 MoP-range ids — MoP is deliberately out of scope per D-10).
- Emit lib/generated/game-data.consumables.ts (178 rows) from a live
  wago.tools run; add a row-count floor at the curation registry size so a
  partial resolution is a hard failure, not a silent gap.
- Consumable-name cross-era collisions resolve Classic+TBC-first (not
  newest-era-wins): live data surfaced 3 ids (28497, 33721, 22756) where a
  genuinely TBC consumable's spell id is reused by an unrelated WotLK/Cata
  spell — the same cross-era ID-reuse phenomenon 02-03 found in the
  enchant/gem data. Newest-wins would have silently mislabeled a real TBC
  elixir.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…lision precedence

- Add lib/generated/index.ts as the single composition point for
  ENCHANT_NAME_DB, GEM_NAME_DB, GEM_STAT_DB, CONSUMABLE_DB and UNVERIFIED_IDS.
- Reduce lib/cla-constants.ts to a thin re-export: no inline id-map literals
  remain; type declarations, CLASS_BUFF_FAMILIES and EXPECTED_TALENT_POINTS
  stay unchanged, and every existing import site works untouched.
- GEM_STAT_DB's name is derived from the composed GEM_NAME_DB rather than
  stored twice, making cla-constants.test.ts's name-sync invariant structural.

Deviation from the plan's literal "later era wins" composition order: eras
compose Classic+TBC-first (earliest-resolved-era wins a collision), not
newest-era-wins. Live wago.tools data proved a flat later-wins merge is NOT
behaviour-preserving at the exhaustive-data scale — enchant id 3003, gem id
32196 and enchant id 2667 all collide across eras with an unrelated item
(RESEARCH.md Pitfall 5), and a later-era-wins merge silently replaces the
real TBC fact lib/cla-constants.test.ts pins with an unrelated item that
happens to reuse the same numeric id. TBC-first precedence is the only
ordering that keeps the test suite's pinned facts correct, and matches
ParseForge's product focus (Classic/TBC analyzer). Every collision is still
enumerated, never silently applied.

A second, larger deviation surfaced verifying this task's own hard gate
(cla-constants.test.ts passing unmodified): consumable buff-aura names
resolved directly from SpellName.Name_lang diverge from the item's
curated display name far more often than expected — 92 of 178 curated
consumables (52%) needed a consumableNames override beyond the 38 already
added in Task 1, split across three patterns: (1) 26 ids where the client
omits the "Elixir of "/"Flask of " item-type prefix from the buff-aura
label; (2) 54 ids, mostly food buffs, where distinct items collapse to an
identical generic buff-aura name ("Well Fed") or the buff-effect name
diverges from the item's flavor name; (3) 2 Cata weapon-enhancement ids
(96264, 96294) whose resolved value looks unrelated to the curated item and
could not be corroborated against any other era — flagged for the
docs/GAME-DATA-AUDIT.md human-check rather than trusted. All 130 total
consumable overrides preserve the pre-regeneration, previously-verified
name; every CONSUMABLE_DB value now matches its pre-cutover name exactly,
confirmed by a full 178-row parity check.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…r review

- Add the --markdown emitter's remaining sections to scripts/regen-game-data.mjs's
  report builder (done in the prior commit): a generated-file "do not edit"
  header, an era-builds table with a previous-build column, a row-count-vs-
  floor table, the consumable-name pass summary, an unverified-overrides
  section listing every UNVERIFIED_IDS entry with its source note, and a
  changed-values-since-previous-run section backed by a
  node_modules/.cache/regen-game-data/previous-run.json sidecar.
- Re-run `npm run regen-game-data -- --markdown docs/GAME-DATA-AUDIT.md` for
  real against live wago.tools. Builds unchanged since 02-03/PR #11
  (2.5.6.69546 / 3.4.5.63697 / 4.4.2.60895); all floors met; 178/178
  consumable names resolved; changed-values section empty (no upstream
  data drift since this session's earlier runs). 809 enchant + 295 gem
  cross-era collisions enumerated, consistent with 02-03's count.
- Reviewed the unverified-overrides list (133 entries: 3 enchant, 130
  consumable) and confirmed every one is either genuinely unresolvable via
  this pipeline's per-id lookups or a deliberate accuracy-preserving
  override — flagged ids 96264/96294 (Cata weapon enhancements) remain the
  one pair whose resolved value could not be corroborated and is preserved
  from pre-regeneration curation pending a future item-name-based
  derivation pass.
- Confirmed idempotent regeneration (byte-identical apart from the
  generation timestamp) and re-ran the full verification gate: 101/101
  tests, tsc clean, touched files lint clean.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
- 9 behaviours for generateSuggestions' new healer-only rules (D-07):
  overheal, uptime, effective-HPS-gap firing/non-firing, DPS active-time
  rule suppression for healers (and preservation for DPS), description
  content, zero-sample-count gating, and priority sort order
- Add "healing" to the ImprovementSuggestion category union so the tests
  typecheck against the category the new rules will emit
- Verified RED: 6/9 new tests fail against pre-change generateSuggestions
  (via temporary stash of the implementation), confirming they exercise
  real behaviour, not vacuous assertions

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…ime rule

- generateSuggestions() gains an optional sixth `healer?: HealerComparison`
  parameter, passed through from buildAnalysisResult's existing
  healerComparison param — the five original parameters and their order
  are unchanged
- The DPS-shaped active-time (ABC) rule is now gated on
  `playerRole !== "healer"`; damage dealers still receive it unchanged
- Three new healer rules (D-07), each gated on the role, the presence of
  a healer comparison, and a nonzero top-healer sample count so an empty
  comparison population produces no relative advice (T-02-21):
  - High overheal: fires when the player's overheal exceeds the top
    healers' by a named ratio (HEALER_OVERHEAL_RATIO = 1.3) AND clears an
    absolute percentage-point floor, so a near-zero top value can't flag
    any nonzero overheal as a fault
  - Low healing uptime: fires when uptime falls below the top healers'
    by a named ratio (HEALER_UPTIME_RATIO = 0.9)
  - HPS gap despite efficient healing: fires only when neither rule above
    fired, using the existing dps.gapToTop/playerDps/topDps figures
    (T-02-22 — never recomputes a gap the comparison card already shows)
  - All multipliers are named module-level constants with rationale
    comments, not inline literals, and are expected to be retuned against
    real logs
- Every fired healer description quotes both the player's own value and
  the top value for that metric

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
- ComparisonSummary.tsx: add `healing: "Healing"` to categoryLabels so the
  new D-07 suggestion cards get a proper badge in the UI and the Discord
  copy, instead of falling back to the raw lowercase category key
- usePlayerAnalysis.ts: analysis_complete now carries player_role, and,
  when the payload includes a healer comparison, overheal_percent,
  activity_percent, top_overheal_percent and suggestion_count — makes
  healer adoption of the new surface measurable at the OPS-01 gate
- Consolidated the two analysis_error call sites (HTTP-error branch and
  exception branch) into a single captureAnalysisError() closure so this
  file keeps exactly two posthog.capture call sites (one success, one
  error), matching the ship gate's one-call-site-per-interaction grep —
  the file previously had three literal call sites for two interactions

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…overview-engine

- lib/cla-engine.test.ts: missing-enchant detection by slot name, flask
  presence (synthetic — no fixture records a buff-uptime table), gem-vs-role
  mismatch, class-buff availability, one full buildCLAResult snapshot — all
  driven by the recorded demo-raid-overview/demo-raid-combatant-info fixtures
- lib/raid-overview-engine.test.ts: healer metrics agreement with
  computeHealerMetrics (D-08), role-then-throughput sort order, death
  timeline ordering/fight-relative timing/non-player exclusion/death-table
  fallback (synthetic events over real player ids — fight 23 has zero
  recorded deaths), one full buildRaidOverview snapshot
- Committed snapshots under lib/__snapshots__/

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…ry and error classification

- lib/wcl-client.test.ts drives the real wclQuery through vi.stubGlobal
  fetch sequences (no MSW, no new dependency) plus a vi.mock of ./kv-cache
  so no Redis env or network access is required
- Covers: token mint + GraphQL success, 401 refresh-and-retry (asserted via
  fetch call count), 429 exhausting all retries -> rate_limited, 500
  exhausting all retries -> upstream, AbortError on every attempt ->
  timeout, GraphQL error text classification (not_found/private/upstream),
  the userMessage boundary (present, no raw-detail leak), and missing
  WCL_CLIENT_ID/WCL_CLIENT_SECRET rejecting with both var names
- Retry/timeout scenarios run under fake timers (vi.resetModules per test
  isolates wcl-client's module-scope token/query caches); full suite stays
  well under 30s
- lib/wcl-client.ts untouched; no new test dependency introduced

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…OPS-01 gate

- Local-gate step now states out loud that a non-zero npm test exit blocks
  a production deploy — no override, no "known failure" allowance
- New subsection names the seven test files that make up this phase's
  reach (cla-engine, raid-overview-engine, wcl-client, timeline-engine,
  healer-metrics, generated game-data guard, fixtures shape guard)
  alongside the pre-existing suite; states explicitly that no
  coverage-percentage threshold is enforced and why
- Records that committed lib/__snapshots__/ snapshots are updated only
  deliberately, with the diff reviewed — a reflexively-refreshed snapshot
  converts the regression net into a green light that certifies nothing

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
alexandermayes and others added 2 commits September 7, 2026 19:50
…O invariants, PostHog grep, game-data review

- Local gate: tsc clean, 135/135 tests, theme-parity PASS, token-audit 0 gate-relevant findings;
  unscoped npm run lint exits 1 but is confined to untracked .codex/ scaffolding (scoped run over
  app/lib/components/scripts is clean apart from one pre-existing documented warning)
- npm run seo-invariants exits 0 for all 11 routes; /analyze/[reportCode]/page.tsx confirmed
  unchanged this phase (last touched a4aaf75, pre-Phase-1) with a param-free canonical
- timeline_viewed, timeline_error, timeline_filter_used and analysis_complete each verified at
  exactly one posthog.capture call site; documents a shell-quoting bug in the plan's own verify
  script that undercounts single-file matches
- docs/GAME-DATA-AUDIT.md read: 133 unverified overrides, 0 changed values since the previous run
  — closes ACC-01's D-12 review-artifact requirement

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
parseforge Ready Ready Preview Sep 8, 2026 9:30pm UTC

alexandermayes and others added 5 commits September 7, 2026 21:51


- Developer decision: preview-first (preview deploy, real-browser sweep, then prod)
- Push + PR already done by orchestrator: origin/main fast-forwarded to 55d2010,
  growth/phase-2-accuracy-depth pushed and PR #15 opened
- Task 3 in this dispatch runs the PREVIEW half only

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…ted checks

- vercel whoami precondition confirmed (alexandermayes)
- vercel deploy --scope loot-list-plus --yes (no --prod) — READY,
  https://parseforge-7yjzs8yw5-loot-list-plus.vercel.app
- Automated route/SEO checks against the preview blocked by Vercel team SSO
  (expected, same as Phase 1's preview) — recorded, not silently skipped
- PostHog/Search Console/production deploy deferred to post-approval half

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
…sign-off

Records the developer's preview sweep approval (11 routes, both themes,
Timeline 375px pass, real-gear game-data names, healer row), the production
deployment facts (dpl_5bwk1fJJNuZXkoC5poPGFZQpGy6c, aliased parseforge.gg),
and post-deploy evidence gathered by this dispatch: seo-invariants re-run
clean against production, an 11-route + sitemap/robots 200 sweep, and a
live POST /api/timeline proving the Timeline path works end-to-end against
production Redis and WCL credentials (castCount/idleThresholdMs match the
02-05 calibration exactly). PostHog and Search Console checks are recorded
honestly as no-data — this dispatch has no PostHog/gscServer MCP tool
available, mirroring 01-09's identical GSC-tool-unavailability note — and
carried forward to the next gate rather than fabricated or dropped.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
Phase 2 shipped to production (dpl_5bwk1fJJNuZXkoC5poPGFZQpGy6c, aliased
parseforge.gg) via a preview-first deploy the developer approved twice.
OPS-01 gate closed with a dated Phase 2 sign-off; PostHog/GSC post-deploy
checks recorded honestly as no-data (MCP tools unavailable to this
dispatch) and carried forward.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FeRkPoQDHb1UFVM3zrRsjs
@alexandermayes
alexandermayes merged commit f1d74f4 into main Sep 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant