Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5e6419f
docs: add deck builder plan + cross-session handoff system (milestone…
Doberjohn Jul 8, 2026
17b2233
feat(deck): Phase 0 wave 1 - type contract (#458) + engine removal de…
Doberjohn Jul 8, 2026
0f65b5a
feat(deck): Phase 0 wave 2 - deckStats, quality score, hoser catalog …
Doberjohn Jul 8, 2026
b5b3043
feat(deck): Phase 1 advisor brain - archetype, analyzers, vulnerabili…
Doberjohn Jul 8, 2026
63b6ddd
feat(deck): DeckContext + deckStorage local draft state (#465)
Doberjohn Jul 9, 2026
11e141d
fix(deck): DeckContext unmount-flush + derive-in-render ink self-heal…
Doberjohn Jul 9, 2026
3bf6767
docs(deck): harden cross-session ritual - start audit, close-out cont…
Doberjohn Jul 9, 2026
d112a6b
docs(deck): add Google + Discord auth setup guide for #463
Doberjohn Jul 9, 2026
e692a6c
feat(deck): #466 deck routes + DeckProvider mount + mobile Decks tab
Doberjohn Jul 9, 2026
1e56dca
feat(deck): Supabase Auth for cloud decks, Google/Discord OAuth (#463)
Doberjohn Jul 9, 2026
aea298a
feat(deck): builder shell + card pool (#467)
Doberjohn Jul 9, 2026
96508f6
feat(deck): pool tile hybrid stepper + motion (#467)
Doberjohn Jul 9, 2026
744640c
feat(deck): pool tile click-to-add + info button, SVG stepper icons (…
Doberjohn Jul 9, 2026
e540325
feat(deck): builder layout rebalance + DeckCardRow/panel design conce…
Doberjohn Jul 9, 2026
9be8dbc
feat(deck): DeckPanel/DeckCardRow transfer, CompactHeader, ink-subset…
Doberjohn Jul 10, 2026
a6c3812
merge: bring master into the deck-builder epic (legal pages #476, CLA…
Doberjohn Jul 10, 2026
9bb028d
docs(claude-md): de-stale the deck-epic working-location rule
Doberjohn Jul 10, 2026
d8a26c3
refactor(deck): clear CodeScene complexity findings across 9 builder …
Doberjohn Jul 10, 2026
6ca173e
perf(size-limit): gate the entry chunk, not the sum of all lazy chunks
Doberjohn Jul 10, 2026
ed643de
feat(deck): profiles + decks cloud persistence, RLS, deckRepository (…
Doberjohn Jul 10, 2026
cbab8db
refactor(deck): clear CodeScene findings on the #464 persistence code
Doberjohn Jul 10, 2026
b79fe87
fix(deck): block cross-account draft migration on a shared browser (#…
Doberjohn Jul 10, 2026
49797fb
test(reveals): skip the seasonal reveals suite off-season instead of …
Doberjohn Jul 10, 2026
67fd7ce
fix(deck): truncate profile display_name so long OAuth names cannot a…
Doberjohn Jul 10, 2026
45d28b5
fix(deck): address CodeRabbit review findings on PR #480
Doberjohn Jul 10, 2026
e9007bf
fix(deck): server-own decks.updated_at on insert so a client cannot f…
Doberjohn Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ apps/web/public/mockups/
# Design handoff reference (Claude Design exports) — local-only, never committed
apps/web/reveals page redesign/

# Security audit findings — sensitive, keep out of the public repo
docs/SECURITY_AUDIT.md

# Tool / plugin local state — managed by their installers, not tracked
.agents/
.superpowers/
Expand Down
35 changes: 35 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,41 @@

Lorcana synergy finder for Core format with archetype-based synergy detection.

## Active Epic: Deck Builder & Engine Score (milestone #3)

The flagship multi-session initiative. **Read these before starting any deck-builder work:**
- **Plan (source of truth)**: [`docs/deck-builder/PLAN.md`](docs/deck-builder/PLAN.md) — full design + rationale (8 phases, E0–E7).
- **Progress**: milestone #3 → `gh issue list --milestone "Deck Builder & Engine Score" --state all`. Epics #450–#457; Phase-0/1 tasks #458–#473.
- **Session Handoff Log**: pinned issue **#474** — the running ledger. **Append an entry every session** (shipped / decisions / gotchas / in-progress / next); read the latest entry at session start.
- **Working location**: the MAIN checkout (`D:\johnn\Projects\inkweave`) on branch `claude/inkweave-deck-builder-af3e5b`. Never work from a `.claude/worktrees/` folder: this epic's commits are unmerged, and `.claude/settings.json` wires the hooks to the main checkout by absolute path, so a worktree silently runs without `branch-verification` or `engine-auto-rebuild`.

**Golden rules for this epic:**
1. Deck-level synergy uses the **precomputed pairs JSON** (`pairs[id].aggregateScore`), NOT the live engine — lazy-import the engine only for hypothetical/preview cards.
2. The advisor is **pool-driven + archetype-parameterized** — detect roles from card text/keywords, **never hardcode card names** (Core rotates).
3. The Deck Quality Score is a **transparent weighted formula + case library**, never ML.
4. Meta/matchups are **deferred** until the user's Set-13 data exists (Phase 6).

**Session ritual (a process to run every session, not a hope):**

_Start:_
1. **Audit the previous session's close-out first.** Is `git status` clean and on `claude/inkweave-deck-builder-af3e5b`? Does the latest #474 comment exist and end with a `Next:` line? Do the issues touched last time have a note? If anything is missing, surface it to the user before starting new work.
2. Read `docs/deck-builder/PLAN.md` and the latest #474 entry (its `Next:` line names your task).
3. `gh issue list --milestone "Deck Builder & Engine Score" --state open`, then pick the next unblocked task. Auth (#463) and migrations (#464) need the user's OAuth apps + live-DB authorization, so skip them when running headless.
4. `/implement-issue <num>`.

_End or any pause (the close-out contract, in order, all of it):_
1. **Adversarial review before committing.** Self-review the diff, or spawn a code-reviewer agent, for correctness, lifecycle/async edge cases, and convention. Fix what it finds. A green suite is necessary, not sufficient (that is how the #465 unmount-flush + async-ink bugs slipped past a passing session).
2. **Commit** (WIP if incomplete). The pre-commit gate (lint + tests) must pass; never `--no-verify`. Nothing important stays only in the working tree.
3. **Append a #474 entry**: shipped (with SHAs) / decisions + why / gotchas / in-progress / **`Next:`**. Always end with an explicit `Next:` line, even on a demo or review entry, so the next session finds the thread immediately.
4. **Comment implementation notes** on each touched issue (decisions, deviations, carry-forward gotchas).
5. **Write a memory file** for any durable gotcha that generalizes beyond this task.
6. **Update `docs/deck-builder/PLAN.md`** if the design or scope changed.
7. **Verify clean**: `git status` clean, scratch files removed, then `/close-session`.

**Epic-issue convention:** the milestone's task issues (#458 onward) are compact pointers; their authoritative spec is `docs/deck-builder/PLAN.md` plus the reference files each one names, not a standalone ops-runbook. Override `/implement-issue`'s defaults deliberately (and say so, do not expand the issue): stay on the epic branch (do NOT cut a `feature/<n>-*` branch, since the work depends on this branch's unmerged commits), and treat its issue-quality rubric as not applicable (it scores production runbooks, not feature-code pointers).

**Retire this whole section when milestone #3 closes.** It is the only always-loaded epic context in this file, and it should leave with the epic rather than becoming another stale block. It earns its place here only because the session ritual has no `paths:` trigger to hang a `.claude/rules/` file on: start-of-session process cannot be lazily loaded.

## MVP Status

- **Scope**: Core format only (sets 9+), community voting, deck builder
Expand Down
3 changes: 3 additions & 0 deletions apps/web/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'react-loading-skeleton/dist/skeleton.css';
// Load the app's @font-face (Plus Jakarta Sans + Tinos, served from /public/fonts
// via staticDirs) so stories render in the real app fonts, not system fallbacks.
import '../src/index.css';
import type {Preview} from '@storybook/react-vite';
import {themes} from 'storybook/theming';

Expand Down
2 changes: 2 additions & 0 deletions apps/web/e2e/E2E_TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ Terminal-state guard for the in-depth vote page (`/vote/:a/:b`). Complements `pa

## `reveals-page.spec.ts` — 8 tests (7 desktop, 1 mobile)

**Seasonal — the whole suite auto-skips off-season.** `beforeEach` reads `/data/previewCards.json` and `test.skip`s when there are no revealed cards (`cards: []`, e.g. after a set graduates into `allCards.json`) or the release date has passed. Skipping happens before any 30s-timeout wait, so the suite is dormant (not failing) whenever there is nothing to reveal, and resumes automatically once the next set's reveal data is populated.

| Test | What it verifies |
|---|---|
| renders the tracker: hero, six ink trackers, and franchise cards | sr-only `h1`, the "Attack of the Vine!" logo, 6 `ink-tracker-tile`s, the "Ink board" section, and the 3 "View … cards" franchise buttons all render |
Expand Down
47 changes: 27 additions & 20 deletions apps/web/e2e/tests/reveals-page.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
import {test, expect} from '../fixtures';

/**
* Why the reveals suite is off-season-gated: the whole page only exists during a
* reveal season, so off-season the assertions cannot pass. Two off-season signals,
* either of which means "skip cleanly", checked BEFORE any 30s-timeout-prone wait:
* 1. No revealed cards at all — previewCards.json cards: [] (the primary signal;
* once a set graduates its cards move into allCards.json and this file empties).
* 2. Past the set's releaseDate — useRevealPhase returns 'released', RevealsGate
* redirects /reveals -> / and the nav entry / promo modal drop.
* Returns the skip reason, or null when a reveal season is genuinely active.
*/
function offSeasonSkipReason(data: unknown): string | null {
const d = (data ?? {}) as {cards?: unknown; sets?: Record<string, {releaseDate?: string} | undefined>};
const revealCount = Array.isArray(d.cards) ? d.cards.length : 0;
if (revealCount === 0) return 'No active reveal season (previewCards.json has no revealed cards).';
const dates = Object.values(d.sets ?? {})
.map((s) => s?.releaseDate)
.filter((x): x is string => typeof x === 'string');
const latestRelease = dates.length ? Math.max(...dates.map((x) => new Date(x).getTime())) : 0;
if (latestRelease > 0 && Date.now() >= latestRelease) {
return `Reveal season ended (latest releaseDate ${new Date(latestRelease).toISOString().slice(0, 10)}).`;
}
return null;
}

test.describe('Reveals page (flag on)', () => {
test.beforeEach(async ({page}, testInfo) => {
// Clear the daily-dismiss key so the promo modal reliably appears.
Expand All @@ -12,27 +36,10 @@ test.describe('Reveals page (flag on)', () => {
});
testInfo.annotations.push({type: 'requires', description: 'VITE_IS_REVEAL_SEASON=true'});

// Skip when reveal season has ended (today is past the set's releaseDate).
// useRevealPhase returns 'released' once now >= releaseDate, RevealsGate
// redirects /reveals -> /, and the Reveals nav entry / promo modal no
// longer render — so these reveal-season-only assertions can't pass.
// After each set graduates and previewCards.json is refreshed with the
// NEXT set's dates, these tests pick back up automatically.
// Skip the whole suite off-season (see offSeasonSkipReason), before any wait.
const resp = await page.request.get('/data/previewCards.json');
if (resp.ok()) {
const data = await resp.json();
const sets = (data?.sets ?? {}) as Record<string, {releaseDate?: string} | undefined>;
const dates = Object.values(sets)
.map((s) => s?.releaseDate)
.filter((d): d is string => typeof d === 'string');
const latestRelease = dates.length ? Math.max(...dates.map((d) => new Date(d).getTime())) : 0;
if (latestRelease > 0 && Date.now() >= latestRelease) {
test.skip(
true,
`Reveal season ended (latest releaseDate ${new Date(latestRelease).toISOString().slice(0, 10)})`,
);
}
}
const reason = resp.ok() ? offSeasonSkipReason(await resp.json()) : null;
test.skip(reason !== null, reason ?? '');
Comment on lines +39 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Skip tests when previewCards.json fetch fails.

When resp.ok() is false, reason is set to null, so test.skip(false, ...) proceeds with the suite. A missing or unreachable data file is a strong off-season signal — the tests will navigate to a non-existent reveals page and fail with 30s timeouts instead of skipping cleanly.

🛡️ Proposed fix
-    const reason = resp.ok() ? offSeasonSkipReason(await resp.json()) : null;
+    const reason = resp.ok()
+      ? offSeasonSkipReason(await resp.json())
+      : 'previewCards.json unavailable — likely off-season.';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Skip the whole suite off-season (see offSeasonSkipReason), before any wait.
const resp = await page.request.get('/data/previewCards.json');
if (resp.ok()) {
const data = await resp.json();
const sets = (data?.sets ?? {}) as Record<string, {releaseDate?: string} | undefined>;
const dates = Object.values(sets)
.map((s) => s?.releaseDate)
.filter((d): d is string => typeof d === 'string');
const latestRelease = dates.length ? Math.max(...dates.map((d) => new Date(d).getTime())) : 0;
if (latestRelease > 0 && Date.now() >= latestRelease) {
test.skip(
true,
`Reveal season ended (latest releaseDate ${new Date(latestRelease).toISOString().slice(0, 10)})`,
);
}
}
const reason = resp.ok() ? offSeasonSkipReason(await resp.json()) : null;
test.skip(reason !== null, reason ?? '');
const reason = resp.ok()
? offSeasonSkipReason(await resp.json())
: 'previewCards.json unavailable — likely off-season.';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/e2e/tests/reveals-page.spec.ts` around lines 39 - 42, Update the
suite-level skip logic around the previewCards.json request so any non-OK
response produces a non-null skip reason, while successful responses continue
using offSeasonSkipReason(await resp.json()). Ensure test.skip in the
reveals-page setup skips the suite for both off-season data and failed fetches,
with a clear reason for the failed request.

});

test('renders the tracker: hero, six ink trackers, and franchise cards', async ({page}, testInfo) => {
Expand Down
Loading
Loading