Repository navigation
Contribution Record
This page is the running ledger of contributions to DeckPal. It tracks what came from agents vs humans, and which human each agent worked on behalf of.
-
Agent-authored commits (repo AND wiki) carry an
On-Behalf-Of: <github-handle>trailer identifying the human the agent works for, plus aCo-Authored-Bytrailer identifying the agent model. -
Human contributors' own commits carry no special trailer -- the absence of
On-Behalf-Ofmeans the commit is directly human-authored. - Wiki page footers name the last agent + human pair that updated the page.
- This ledger is append-only. Agents append one line per work session.
| Date | Agent | On behalf of | What |
|---|---|---|---|
| 2026-08-09 | Claude (opus agent) | @cheyras | Initial wiki authoring: created all pages from repo design docs, set up attribution system |
| 2026-08-09 | Claude (opus-4-6 agent) | @cheyras | Dependabot triage: merged 8/8 PRs (#3-#10) -- actions/setup-node 7, actions/checkout 7, pnpm/action-setup 6, github/codeql-action 4, grouped npm minor+patch (9 pkgs), @types/node 26, node-cron 4, typescript 7.0. All locally verified (typecheck, test:deck 49/49, builds). |
| 2026-08-09 | Claude (opus-4-6 agent) | @cheyras | Vercel deployment: created deck-pal team project, set 14 env vars (prod+preview), fixed TS 7.0 @vercel/node crash (pre-build API + .mjs entry), fixed .vercelignore root anchoring, dynamic base path for cloud/self-host. Preview + production deployed and verified (API health OK, auth 401 OK, SPA renders login page). Git connect pending owner GitHub Login Connection. |
| 2026-08-09 | Claude (opus-4-6 agent) | @cheyras | fix(cloud): auth-page reload loop (88 navigations/20s). Root cause: AppShell's ProfileChip fired api.overview() on /auth -> 401 -> handle401 -> window.location.assign('/auth') -> hard reload loop. Fixed handle401 redirect guard, stripped AppShell chrome on /auth, made SW paths dynamic (BASE from self.location), cleaned /pokedex/ favicon vestiges. Verified: login completes, real collection data renders (desktop + mobile). |
| 2026-08-09 | Claude (opus-4-6 agent) | @cheyras | End-to-end domain verification on deckpal.app: HTTP checks (apex 200, www 308->apex, /api/health ok+db up, /api/series 401), Playwright login+session stability (no reload loop, 0 nav delta/20s), app shell confirmed (20 series, Mega Evolution 13.5%, Trainer 38), desktop+mobile screenshots, bug reporter E2E (GH issue #11 created, privacy verified -- no email/user-id in issue body, Report-ID present), session persistence after reload. Found defect: bug_report.github_issue_number NULL despite GH issue created (likely RLS blocking UPDATE). No logout control exists yet. |
| 2026-08-09 | Claude Opus 4.6 | @cheyras | Cleanup/close-out wave: fresh-Supabase-build preflight fix (migrate.ts orphan cleanup before 021 FK), migrate-to-cloud singleton fix (ON CONFLICT DO UPDATE for user_profile/user_settings), @types/node-cron removal (node-cron v4 ships own types), engine package rename @pokedex/engine -> @deckpal/engine, PWA verification (manifest+SW+registration all green), DECISIONS.md cloud-connection-day entry, wiki sync. |
| 2026-08-09 | Claude Opus 4.6 | @cheyras | Two cloud fixes: (1) bug_report.github_issue_number never persisted -- RLS blocked UPDATE via q1(); fix: RESET ROLE on rlsStore client (same txn, sees uncommitted INSERT, bypasses RLS), then restore 'authenticated'. Verified live: row ba3493cf has github_issue_number=13. (2) No logout control existed -- added sign-out button on Profile page (cloud-only, hidden in self-host), with logout icon in Icon.tsx. Playwright-verified: button visible, click -> /auth, reload stays logged out, re-login works. Closed GH issues #12 + #13 (test reports). |
| 2026-08-10 | Claude Opus 5 | @cheyras | Issue #15: removed the hand-drawn McDonald's Golden Arches SVG (a third-party trademark reproduced as app chrome). Established TCGdex publishes no logo for any of the 12 McDonald's Collection sets in any of 14 languages; rejected pokemontcg.io (serves McDonald's corporate logo, identical bytes across 9 sets) and Bulbagarden (2 of 12, fair-use tagged) on trademark grounds. Replaced with the real TCGdex set symbol on the series card via a rep/repHasLogo fallback (Trainer kits fixed too, 17 logo-bearing series unchanged) plus a typographic year tag for the rest. manifest:check clean on disk (47,924/47,924) and object (2,946/2,946) tiers; no bytes added. Browser-verified at 1440 + 390 locally; deployed deckpal.app render pending a QA account. |
| 2026-08-10 | Claude Fable 5 | @cheyras | Public marketing landing page for logged-out visitors: built an AI Gateway image-generation pipeline (candidate/start-at budget controls, fail-fast on account-level gateway errors) and five DOM/CSS/SVG product mockups that render complete with zero image bytes; corrected the landing copy to quote the English-only catalog counts (20,964 cards / 203 sets) instead of raw table totals that included Pokemon TCG Pocket. |
| 2026-08-10 | Claude Fable 5 | @cheyras | Auth surface polish: /auth gained URL-driven signup/forgot modes and inline validation, /auth/reset handles Supabase's implicit-flow recovery link (fragment tokens, the PASSWORD_RECOVERY timing race), /signed-out is a real confirmation page, Profile gained an in-app change-password card, and GoTrue error codes now map to one actionable sentence each. |
| 2026-08-10 | Claude Opus 5 | @cheyras | Cloud image tier: added the serverless route deckpal.app was missing entirely -- every card image and set logo had been serving the SPA shell as image/html. /deckpal/images/* now lazily fills the public card-art Supabase Storage bucket from image_asset.source_url on miss and 302s to the object on hit, sharing path/cache-key logic with self-host via a new @deckpal/storage package. |
| 2026-08-10 | Claude Opus 5 | @cheyras | Image tier round two: sprites now fill from the pinned PokeAPI commit, cold fills try the .webp -> .png -> .jpg extension ladder (TCGdex re-encodes per extension), a targeted storage:backfill script mirrors the local cache into the bucket, Vercel functions pinned to sfo1 to co-locate with Supabase's us-west-2 (~90ms -> ~20-25ms per DB round trip), and the Pokedex species-detail route fixed (it called /insights/deckpal instead of /insights/pokedex). |
| 2026-08-10 | Claude Opus 5 | @cheyras | Page-load perf: measured against prod and fixed four round-trip sources -- batched the RLS BEGIN/set_config/SET LOCAL sequence into one round trip, parallelized the 9-query /cards/:id fan-out into one statement, rewrote /series's set/card counts as a CTE (661ms -> 46ms), and inlined set-page variant/owned-quantity data so the grid stops firing one GET per tile. Also fixed the service worker's image CacheFirst route, which matched a path cloud never requests (0/18 images served from cache before the fix). |
| 2026-08-10 | Claude Opus 5 | @cheyras | image_object: split image_asset's identity/provenance from the physical facts of each stored copy, since the disk and Supabase Storage tiers legitimately disagree on re-encoded bytes. Migration 025 adds a per-(cache_key, tier) row; manifest:check --object-store makes the object tier's "no byte without a row" claim falsifiable for the first time. Backfilled 47,924 disk-tier and 2,835 object-tier rows, then audited the 1,854 images with no resolvable upstream URL byte-for-byte against Supabase's MD5 etag -- all matched, nothing needed re-filling. |
| 2026-08-10 | Claude Opus 5 | @cheyras | Issues #18/#19: user_set_progress had RLS enabled with a SELECT-only policy, but recomputeSetProgress() writes it inside every collection mutation -- increment, decrement, set-quantity and have/need all 500'd on cloud with the UI's optimistic count reverting. Migration 028 adds INSERT/UPDATE policies scoped to auth.uid(); audited every other RLS table for the same missing-write-policy shape and found no second instance. |
| 2026-08-10 | Claude Opus 5 | @cheyras | MCP goes multi-user: personal access tokens (dsk_... prefix, sha256-hashed, shown once) at /mcp let any signed-up user point Claude at their own collection through the existing 21 tools, each running inside that user's RLS transaction. Then made claude.ai's non-beta connector dialogs work too -- /mcp accepts the token as the last path segment as well as a Bearer header -- and wrote the six-step setup/verify/revoke guide now in Profile -> Agent access and DEPLOYMENT.md. |
| 2026-08-10 | Claude Fable 5 | @cheyras | Shipped the landing page's marketing imagery (6 assets picked from 18 flux-2-pro candidates, responsive AVIF/WebP, largest 212KB), fixed .vercelignore's blanket .webp/.avif rules that were silently dropping them from the upload, implemented real libpq PGSSLMODE semantics in makePool (node-postgres mapped every verifying mode to full chain verification, breaking the exact Supabase connection command DEPLOYMENT.md documents), and retired scripts/storage-backfill.mjs (bypassed the B1 provenance choke point) in favor of the supported storage:backfill module command. |
| 2026-08-10 | Claude Opus 5 (1M context) | @cheyras | Self-host regression: the cloud pivot's ~30-route rewrite from defaultUserId() to req.user!.id was correct in cloud but broke self-host outright (req.user is undefined there by design; the assertion erased at compile time and undefined hit WHERE user_id = $1, 500ing /insights/overview and every collection/deck write). Introduced apps/api/src/identity.ts as the single identity seam with a CI guard that scans routes for regressions; also fixed deck_version/battle_log writes that had been violating a NOT NULL user_id since migration 020, on both deployments. |
| 2026-08-10 | Claude Opus 5 (1M context) | @cheyras | Rewrote the landing page to lead with the MCP connector instead of burying it below five feature sections: new hero eyebrow/headline naming the mechanism, a new AgentMockup showing an assistant reply citing the visitor's own shelf with reconciling arithmetic, and a new AgentFlow section spelling out token -> connector -> ask and the 21 tools. |
| 2026-08-10 | Claude Opus 5 (1M context) | @cheyras | Verified the deck/battle-log cloud write path end-to-end through a real session, then fixed issue #17 (bug reporter screenshots silently omitted): html2canvas 1.4.1 couldn't parse Tailwind 4's oklab() color functions (moved to the html2canvas-pro fork), and the card-art redirect to Supabase Storage was tainting the canvas (fixed by inlining every image to a data: URL before capture, read through a dedicated CORS-safe ?bugshot=1 marker). |
| 2026-08-10 | Claude Opus 5 (1M context) | @cheyras | Issue #21: the catalog had rotted 2.5 weeks stale (222 cards missing across 10 sets) because nothing refreshed it, and the importer itself couldn't ingest a refresh -- upstream had re-keyed the four SWSH Trainer Gallery subsets without renaming them (froze the whole import on a UNIQUE collision) and reshuffled retired card_variant sort_order (detonated a DEFERRABLE constraint at COMMIT). Both are now detected and handled in place rather than aborting the run. |
| 2026-08-10 | Claude Opus 5 (1M context) | @cheyras | Issue #14: profile photos -- upload/replace/remove, a public 32-hex-key Supabase bucket (image/webp, 1MiB cap), server-side magic-byte validation plus a mandatory sharp re-encode to 256x256 WebP (the real content check, the EXIF/GPS privacy fix, and the weight fix all at once), and a documented B1 exception since avatars have no image_asset-shaped provenance. Browser testing caught a second bug: the avatar disc rendered empty for the length of a fetch because the img/glyph was an if/else instead of a layered fallback. |
| 2026-08-10 | Claude Opus 5 (1M context) | @cheyras | Issue #20: the hosted card scanner matched nothing, not because the in-memory index broke as predicted but because it shelled out to ImageMagick, which a serverless function doesn't have. Swapped to sharp, which forced an ALGO bump and full re-index (0-9 bit hash divergence from ImageMagick) and two 3x SQL-matcher cost fixes; kept the Hamming threshold at 9 based on a measured 389-scan degradation curve. |
| 2026-08-10 | Claude Opus 5 (1M context) | @cheyras | Issue #21 (art side): re-keyed the four SWSH Trainer Gallery sets' cached art to follow the catalog's rename rather than re-fetching (upstream 404s both the old and new ids, so a re-fetch would have deleted 120 cards' art, not restored it) across both the disk and Supabase Storage tiers. Then scheduled the fix that should have caught the staleness: .github/workflows/catalog-refresh.yml runs weekly, deliberately fails the job on an unhandled rename after the import has already committed, and prints the exact rekey:set commands to run. |
| 2026-08-10 | Claude Opus 5 (1M context) | @cheyras | Issue #16: the Pokemon Organized Play logo and 19 other set logos are inked for white cardboard (black wordmarks, no light stroke) and disappear on the app's dark surfaces. Measured all 157 cached logos by orphaned-ink color distance to find the 20 affected; added a that puts a light plate behind exactly those, decided offline and baked into a static id list rather than runtime image analysis. |
| 2026-08-10 | Claude Opus 5 (1M context) | @cheyras | Issue #21 sibling: the same upstream SWSH Trainer Gallery rename that stranded card art had also stranded the deck lane -- ptcgl-set-alias.json and one banlist entry still pointed at the four retired set ids, so resolveDeck() silently resolved "Flareon BRS-TG" to an unrelated Scarlet & Violet promo instead of erroring. Fixed with a pure regression test sweeping both data files for the retired ids, wired into CI. |
| 2026-08-10 | Claude Opus 5 (1M context) | @cheyras | Made the full catalog (23,546 cards, every set, price, and the Pokedex) browsable without an account -- previously invisible behind the signup wall for no reason. The real work was closing the leakage every catalog route had quietly grown (per-user completion rollups, ownership flags, capture state): added a second identity accessor that resolves to null rather than undefined for anonymous requests, and found while doing so that the connection pool connects as postgres, which owns every table and therefore bypasses row-level security entirely -- anonymous requests now run under SET LOCAL role = 'anon' so RLS actually fires. Also fixed a stray unauthenticated GET /api/avatar firing from the mobile drawer before session state settled. |
| 2026-08-10/11 | Claude Opus 5 (1M context) | @cheyras | Issue #29: built a real OAuth 2.1 + PKCE + dynamic-client-registration authorization server for /mcp (oauthServer.ts, routes/oauth.ts, Authorize.tsx, migrations 031-033) so any MCP-spec client connects with one click instead of copy-pasting a token -- the token endpoint just calls the existing createToken(), so it's a bridge onto the pre-existing credential, not a parallel one. An independent Opus review before shipping caught a real bug (session routes reading the RLS-scoped client, which migration 033's zero-policy lockdown would have blocked in production despite passing every local test) and a real open redirect (/\evil.com bypassing a //-only check); both fixed. Verified end-to-end against production with the QA account. A follow-up deploy then hit a stale Vercel build cache that 500'd every signed-in user (unrelated code, --force fixed it); root-caused and reverified within the hour. Synced DEPLOYMENT.md, SPEC.md, SECURITY.md, ARCHITECTURE.md, README.md, and this wiki (MCP-Setup, Decision-Log) to match; strengthened AGENTS.md's doc-sync checklist into an explicit table since the wiki had gone stale before this pass caught it. |
| 2026-08-11 | Claude Sonnet 5 | @cheyras | Issue #25: Profile and the header chip hardcoded the literal "Trainer" instead of the signed-in user's name. Stopped mid-fix to report that the obvious approach (reading session.user.user_metadata.username) was actually broken -- verified against the live DB that only the QA account (admin-created) has that metadata set; the 3 real signups have it empty, since Auth.tsx's signUp() never writes it. Per direction, added GET /me in apps/api (matching the codebase's cloud/self-host abstraction-layer convention over a first-of-its-kind direct supabase.from() call) reading app_user.username, the value the signup trigger's COALESCE fallback always populates. Verified in a real signed-in browser session (QA account, both 390px and 1440px) after working around two local-only obstacles: no Vite dev proxy for /api (cloud path), and vercel dev's local function runtime failing on an opaque DB-permission error unrelated to the fix. Also surfaced, but left unfixed as out of scope: a pre-existing Promise.all race in /insights/overview (two queries sharing one RLS-scoped PoolClient) that wedges the connection pool under a persistent single-process local run. |
| 2026-08-11 | Claude Sonnet 5 | @cheyras | Issue #26: "/insights range chips don't change anything" turned out to be true and honest -- the one real account with any history has only 10 days of it (07-30 .. 08-08), so 30d/3m/6m/1y all resolve to the identical rows. Rejected both easy-but-wrong fixes (hiding the feature behind a flag, or inventing back-dated points) in favor of a small pure rangeCoverageCaption() helper plus one new line under the chart: "Showing all N days of recorded history (started )" whenever the real span is shorter than the selected window. Added apps/web's first-ever unit test (node --import tsx --test, mirroring apps/api's convention) covering the exact reported scenario. Verified live against the QA account: 0-point and 1-point cold starts unchanged (regression-clean), then seeded 5 rows for the QA user only to reproduce the actual bug -- 30 Days/3 Months/1 Year rendered pixel-identical charts, each now carrying the caption -- deleted afterward, row count confirmed back to 0. Independently re-hit and corroborated the Promise.all//insights/overview connection-pool race from the #25 entry above (a real page load fires ~6 concurrent authenticated requests against a 2-connection pool); also confirmed there is currently no live automated mechanism producing collection_value_point rows for any cloud account at all (no Vercel Cron, no GitHub Action, the only cron needs a persistent process nothing here runs) -- both logged as separate, out-of-scope findings, not fixed here. |
| 2026-08-11 | Claude Sonnet 5 | @cheyras | Issue #27: removed the never-shipped Stream Tools feature (OBS overlay pop-up) end to end -- the nav's disabled "Soon" entry, the stream icon, routes/Overlay.tsx, its route registration, its /overlay chromeless-path entry, and the now-orphaned collectionEvents API wrapper (confirmed sole caller before removing), plus one stale comment beyond the traced list. Deliberately left GET /collection/events and its tests alone (general attribution log, not stream-specific -- confirmed via its own doc comment and ?source=deckpal-mcp test coverage) and left the two research/*.md mentions alone (competitor research notes on pkmn.gg's own Stream Tools, not a DeckPal spec). Verified live against the QA account at 1440px + 390px on /series: no Stream Tools row at all; /overlay now falls through to the router's default "Not Found." Re-hit (and left unfixed, same as #25/#26) the known Promise.all//insights/overview pool-wedging bug while standing up the local verification harness. |
| 2026-08-11 | Claude Sonnet 5 | @cheyras | Issue #30: reviewed and finished a prior agent's uncommitted diff (that session stalled 7 hours on browser verification and was killed) consolidating ProgressCluster's two fixed bars into one bar retargeted to the selected goal, with a per-goal color/badge; the diff itself was solid on review. Verified live against the QA account (2 owned variants seeded via the real collection UI, cleaned up after) across all three goal states -- one bar only, correct color/badge/numbers each time. Along the way, re-hit and further characterized the recurring Promise.all//insights/overview pool-wedging bug (#25/#26/#27 above): confirmed it fires globally on every authenticated page (AppShell calls it, not just the Insights page) and wedges more endpoints than previously logged (username/avatar/dex-capture/series-count queries all leaked a connection across repeated attempts) -- likely the actual explanation for the prior agent's stall. Verified around it this time by serializing all /api/* calls through Playwright's request routing rather than waiting on it. Corrected UI-Spec.md §3.6 (still-accurate pkmn.gg research, now flagged as superseded for DeckPal's own component) and this Decision Log. |
| 2026-08-11 | Claude Opus 4.6 (1M context) | @cheyras | Issue #31: agentic deck-building now defaults to cheapest printing. Three coordinated changes in deckpal-mcp: (1) resolve.ts joins best USD market price per candidate card, describeCard() renders it, ambiguous candidates sort price-ascending (unpriced last); (2) search_cards groups same-name rows cheapest-first and its tool description tells agents to prefer cheapest printing; (3) save_deck/ptcgl_text/cards field descriptions carry the same cheapest-printing guidance. Verified end-to-end against the live DB: "Mega Lucario ex" (7 printings from $0.78 to $272.52) now returns cheapest first in both get_card and search_cards. |
| 2026-08-11 | Claude Opus 4.6 (1M context) | @cheyras | Issue #28: root-caused "No image was uploaded" avatar/scanner failures to a Vercel-specific race -- NODEJS_HELPERS's lazy req.body getter drains the request stream on first access, so express.raw() reads an already-empty stream and overwrites the valid body with a zero-length Buffer. Fixed by snapshotting req.body before express.raw() runs and restoring it if the raw parse comes back empty; ruled out express.json(), RLS middleware, and a Mobile-Safari-specific bug via Vercel production logs (failures uncorrelated with client, and the scanner route -- camera frames, not a file picker -- failed identically). Verified against a real Vercel preview deployment: concurrent octet-stream/JPEG/PNG uploads and the scanner endpoint both now succeed; an empty body still correctly 400s. |
| 2026-08-11 | Claude Sonnet 5 | @cheyras | Issue #32: the in-app reporter now carries kind: 'bug' \| 'feature' end-to-end -- migration 034_bug_report_kind.sql, a generalized labelsForKind() filing every issue with the umbrella in-app-report label plus bug or feature-request, and a segmented toggle on the frontend driving the modal copy. Verified against a local migrated dev Postgres wired to the real GITHUB_TOKEN (production Supabase never touched) by filing a real test issue (cheyras/deckpal#36, both labels confirmed) and closing it. |
| 2026-08-11 | Claude Opus 4.6 (1M context) | @cheyras | Issue #33, scoped down by the product owner before any code was written: built a comment-only AI triage bot (.github/workflows/issue-triage.yml + scripts/triage-issue.sh) that posts a Haiku-4.5-generated draft analysis on every reporter-filed issue, bounded to Project-Brief.md + recent Decision-Log context, clearly labeled "draft -- for maintainer review, not authoritative." Explicitly deferred: Playwright bug reproduction with QA credentials in CI, and any automatic labeling/closing -- both judged too risky for a first pass. Dormant until ANTHROPIC_API_KEY is added (gh secret set ANTHROPIC_API_KEY --repo cheyras/deckpal); honestly flagged that no live end-to-end run was possible without that secret. |
| 2026-08-11 | Claude Sonnet 5 | @cheyras | Issue #34: built a reusable KebabMenu component and used it to bury the standalone, always-visible "Delete deck"/"Delete list" buttons that used to sit exposed next to each entity's title, in DeckBuilder.tsx and ListDetail.tsx -- the only two places matching the reported pattern. Confirmed several look-alike buttons (per-row list-item removes, showcase-card remove, token revoke, an already-progressive-disclosure "Delete Log") were a different UX context and left them alone. Same confirm-modal flow underneath, unchanged. Verified live against the QA account with a throwaway deck and list, at desktop and 390px. |
| 2026-08-11 | Claude Opus 4.6 (1M context) | @cheyras | Issue #35, and the actual fix for a bug four separate agents had independently stumbled into and documented earlier the same day (#25/#26/#27/#30): Promise.all over multiple q() calls sharing one per-request RLS PoolClient doesn't run in parallel -- node-postgres serializes on one connection, and the confused concurrent dispatch can wedge it, starving the API's 2-3 connection pool. Since AppShell fires /insights/overview on every authenticated page, this alone explained the felt slowness on /decks (which has no Promise.all of its own). Fixed the three remaining instances following the proven cards.ts precedent -- two folded into single combined-SQL round trips, one switched to sequential awaits where the JS post-processing made combining not worth it. Verified byte-for-byte identical JSON output against the real DB (empty and populated accounts) and 30 concurrent requests with zero failures; honestly noted the QA account's empty collection made the original race too fast to freshly reproduce in a test harness, so the fix's confidence rests on the code-pattern match plus the four prior agents' field evidence. |
| 2026-08-14 | Claude Fable 5 | @cheyras | Finished the design-system branch to production: full pre-merge review (8 finder angles + adversarial verification) with six fixes — RLS rollback paths now destroy a possibly-in-use client (cross-user exposure), Button/Tabs default type="button" (Cancel buttons were submitting their modals' forms), direct-Postgres pools regained the hard cap 3, PGPOOL_MAX no longer leaks into request pools, dev.mjs builds storage and checks exit codes, premium grain moved to a compositable fixed overlay (pixel-identical by RMSE). Shipped /design to production gated to the owner account via a server-verified designEditor flag on /me (DESIGN_EDITOR_USER_ID); read-only there by construction. Merged main's #28–#35 wave into the branch (one-bar ProgressCluster re-expressed with the ProgressBar primitive), synced AGENTS.md B2, ARCHITECTURE.md §14, DEPLOYMENT.md, this wiki. PR #38. |
| 2026-08-18 | Claude Opus 5 | @cheyras | Ported Deck-E, the animated assistant character, from Blender to three.js as a dev-only preview at /dev/decke — all 27 authored states, the analytic eye shader (a 215-node Blender graph rebuilt as GLSL, since it exports as a flat useless material), the three seeded procedural layers (idle float / blink / gaze), the facing-yaw mirror system, and a flight solver that parks him beside an arbitrary DOM element facing inward, behind a JSON command surface an LLM can drive. Chose to drive 47 normalised pose channels per frame rather than AnimationMixer clips, because the channel semantics are exactly what the eventual agent driver needs; recorded in DECISIONS.md and Architecture §15. Verified numerically rather than by eye: parity fixtures generated by EXECUTING the character wiki's own Python (field 1e-9, PRNG/float/blink 1e-12) and image comparison against Blender renders at a matched camera, silhouette IoU 0.89–0.99 across eight poses. That measurement caught five bugs code review had not — riders computed in world space (his eyes stayed behind when he flew), state position overwritten by the parked anchor, a camera axis conversion worth 76 px (objects are C·R·C⁻¹, cameras C·R), brow height written to Ctrl_Brows_anim which turns out to be a sibling of the brow sockets reached only through non-exporting drivers, and an unprefiltered environment that was both aliasing the eyes and over-contributing 5% of luminance. Also found and logged eight stale numbers in the character wiki (its own regeneration chain died on 2026-08-17, so the .blend is now the authority) and three beliefs of my own that measurement refuted. Rescued the scene's HDRI, which was living in a Windows temp file that would not have survived a reboot. |
| 2026-08-18 | Claude Opus 5 | @cheyras | Finished Deck-E and verified it against the .blend rather than by eye. Five defects closed, and in four of the five my own recorded diagnosis was the thing that had to be discarded first. The lid pivot is a matrix pair, not an angle pair — DeckBox_Lid.location is keyed in the file and reaches (0, 0.152263, -0.117046) at the full gape, so driving only rotation.x had left the lid 0.313 BU out of place; that alone was why card_stash was the worst frame in the sweep. Eye_Rig is VERTEX_3-parented to the MORPHED lid, which no analytic field can stand in for; the port now reproduces the file's world matrix there to 1e-6, and the eyes stop drawing on the inside of the open lid. The brow sockets hang off Eye_Rig, not the lid (the character wiki says otherwise and is wrong), and carry a keyed follow-through of their own — modelling both took the worst brow error from 0.36 BU to 0.006. The lighting residual was not occlusion at all: filed for weeks as "shadows 24-37% too bright", it was an unported EEVEE firefly clamp (clamp_surface_indirect = 10.0) that has to be applied to the HDRI texels rather than the finished IBL lookup — capping the lookup moves the scene 0.08%, capping the texels fixes it, and the giveaway was bucketing the residual by surface NORMAL instead of by pixel (up-facing lid top +44%, front face +5%, a shape no shadow term can make). Zero per-frame cost. And a production bug that had nothing to do with the character: gating the dev route with beforeLoad left its import() reachable, so rollup emitted the chunk and vite-plugin-pwa precached it — every production user was downloading 945 kB of three.js for a route that 404s. Building the route inside an import.meta.env.DEV guard took the precache from 2887 to 1964 KiB. Also shipped the asset (7.48 -> 2.92 MB, meshopt; NEVER quantize — it parks a de-quantisation transform on nodes the rider system overwrites, and inflates a hinge pin into a cylinder wider than the character), and found that travel_far's flight is baked into the .blend at 2x speed, where marker spacing across the whole timeline proves the port is the correct one. Verification went from 8 stills to 14 stills plus 3 clips compared frame-by-frame, and I pinned the blink cursor and float clock after discovering the harness was non-deterministic to 0.045 IoU — enough to hide a regression or invent one. The harness now lives in the repo with a Blender script that regenerates its references, instead of in a temp directory that would not have survived the session. 48 tests, 37 draw calls, 5.8 ms median frame at 3x DPR under 6x CPU throttling. Blender was read-only throughout. |
| 2026-08-19 | Claude Opus 5 | @cheyras | Root-caused and fixed the silent-success incident, then everything two agent reports asked for. log_cards was a loop costing 0.65 s/item against a ~60 s serverless budget, so a 99-item batch outran the wall clock, the response died, and the already-committed writes stood — the agent retried and quantities inflated 4x. Proved it three ways (forensics on collection_event: 91/92/99 events at 58.9/58.9/64.6 s; a live repro that died at 60,060 ms with 87 of 99 committed; the Vercel log showing one increment invocation per item at 0.68 s). Replaced the loop with POST /collection/batch — one transaction, one recompute per DISTINCT SET — and batched resolution: 99 items now 2.9 s on production. Added idempotency keys (bucketed by time, so a retry collides and a genuine re-pull next month does not), a mutation log with before/after snapshots that is append-only at the RLS policy level, mutation_history/revert tools that REFUSE rather than half-apply where an exact undo is impossible, and soft delete with a Recently-deleted section on both indexes. Also found by probing TCGplayer's live endpoint that Mass Entry is ALL-OR-NOTHING and that name lines fail whenever a card name repeats in its set — 40 Pitch Black cards added 0/40 in the old format and 40/40 as <qty>-<productId>; the full cart now replays as 111 listings / 151 copies / 0 invalid. Two adversarial review rounds; the second found that my own idempotency key never expired, that a revert could write a nondeterministic quantity while logging a clean chain, and that dry runs were writing rows — all fixed and re-verified on production before finishing. |
| 2026-08-20 | Claude Opus 5 | @cheyras | Swept the ten open in-app reports (#46–#54, #57), verifying each against the RUNNING product before writing code — production first, the local stack second. Four turned out to need no code, and establishing that was most of the work. #46 and #47 were the recovered design-system stash that landed as 6baf4cc (pre-rewrite SHA; resolves against deckpal-mirror-2026-08-31), filed the day before it and never closed; #53 (the LVL badge shoving the species name down) was collateral damage from premium.css being unlayered and had been healed by the #44 layering fix — all three confirmed correct on deckpal.app and closed with the commit that fixed them. #49 ("a lot of animation I'm not seeing") I left OPEN: diffing the design-system base fcbef90 (pre-rewrite SHA; resolves against deckpal-mirror-2026-08-31) against HEAD proves nothing motion-related was lost — every removal is a documented replacement, including background-attachment: fixed → fixed-position overlay made precisely because iOS Safari ignores the former — and px-rise/px-draw/px-ping/sheet-scrim-in/sheet-panel-up plus the nav transitions all measurably run on cold load AND client-side navigation. The one remaining explanation is the reporter's own Reduce Motion, which the skin deliberately honours; an agent must not close a report by asserting a device setting it cannot see. Six fixed: the mobile drawer passed a hardcoded active={false} so on a phone — where the drawer is the only nav — nothing was ever highlighted (#52, now one isNavActive for all three callers); / sent lapsed sessions to the marketing pitch for an account they already have, fixed with one localStorage bit written from the single module that owns the Supabase client and cleared only by explicit Sign out, never by the expiry path (#50); deck rows now show the code PRINTED on the card (PBL) beside TCGdex's internal ME05, resolved through the exporter's existing verified alias table rather than the abandoned card_set.ptcgl_code (#57); ♀/♂ are drawn as cap-height inline SVG on the baseline because Figtree has no glyph and the per-glyph fallback's metrics dropped the mark into the 18px truncate box that then sheared it (#54); the deck kebab moved into the badge row so items-center does the alignment instead of a 7px offset nobody was maintaining (#48); and the series reveal control is one-way (#51). Verified in headless Chromium at 390/428/1280: kebab-to-badge delta 0 at all three, PBL/SFA/ASC/TEF on the real deck the report came from, reveal button and rule gone (5→20 cards), data-active true in rail AND drawer, gender marks 5px inside the box bottom, and all four / cases. The API change was exercised against a local Postgres, not the live backend, per B12. |
| 2026-08-20 | Claude Opus 5 | @cheyras | Worked a narrated screen recording of /dev/decke — about twenty notes — and found that nearly all of them were one architectural mistake said in different words on different buttons: the port had emotes but no states. A clip played once and held its last beat forever, and because almost every clip's last beat IS the rest pose, "be happy" resolved to "be briefly happy, then be nothing"; the two clips whose last beat is not rest froze on a yawn and on a mouthful of orbiting cards. A state now runs intro → sustain → outro with one loop window per state, and that is not new animation — every oscillation the reviewer asked to keep was already in the MIDDLE of its clip, where play-once-then-hold could never reach it twice. A test evaluates both ends of every window and fails if they disagree by more than that channel's budget; it caught three, including a loading window that would have despawned and respawned both orbiting cards every 1.8 s — the reported defect, reintroduced from the other end. There was no idle: boot was the entry state, 640 ms long, float_amp: 0, blink_rate: 0, and nothing ever left it, so a freshly loaded page showed a character who was not breathing, blinking or looking at anything, ever. And the look-at constraint had never been ported — Ctrl_Target tracks the camera in the .blend, constraints do not export, the exporter emitted it as a childless ROOT node, so rig.ts faithfully wrote the gaze onto an object nothing reads and the pupils sat at their bind pose, which is a baked SAMPLE of that constraint. Worth recording as a class: fourteen parity poses and three clips could not see this, because a frozen constraint and a live one agree EXACTLY at the frame the freeze was taken; what found it was a human watching him turn his head. Two more symbol defects turned out to be one wrong axis (sym_spin about three's Y is blender's Z, an axis INSIDE the glyph plane — it tips the spiral edge-on rather than turning it, and the same 180° right-eye phase about it is a pure horizontal mirror, invisible on a star and a backwards $ on the money glyph), and loading's spinner had never turned at all because nothing read spin or spinner_deg_per_s. Also: flyTo now parks OUTBOARD of its element and looks back across it, home is the bottom-right of the viewport rather than the world origin, the turn is 1.75× faster, talk ramps instead of cutting the jaw off mid-syllable, the crossfade is 320 ms and eased, confused wears spiral eyes swapped in behind a blink, the stash cards' two-frame spawn pop is time-warped to 280 ms and they hang on independent floats, the card fronts get thin-film iridescence over a roughness that is not glTF's default 1.0, and an agent can author its own clip with op: "keyframes". The highlight ring landed in the DESIGN SYSTEM rather than beside the character, which is where it was asked for. An adversarial review of my own first pass then found four more, three of them the same root cause in a new place — the unwrapped state clock leaking into things only meaningful against the authored clip timeline, worst of which spun the orbiting hands back twenty-two revolutions in half a second after a minute of loading. The character tests existed before this and were only ever run by hand, which is exactly how a look-at solve that nothing consumed survived a full parity pass; they run in CI now. 66 tests, PR #60. |
| 2026-08-20 | Claude Opus 5 | Deck-E, second review pass | Thirty-odd notes from a narrated screen recording. The big one: a sustain is now its own CYCLIC clip whose two ends are the same beat by construction, so the loop cannot pop — curious, proud, happy and listening measure exactly zero frame-to-frame motion while sustaining, where they were dropping up to 3% of his height once a second. What was authored to tick still ticks. Second: how he is SEEN is now a separate solve from where he stands, so he keeps his 3/4 yaw and stops leaning wherever he parks (6-22 degrees of lean down to 1-3, and the residual is perspective taper rather than roll), while his VERTICAL angle deliberately still follows his height on the page. The lighting rig and environment ride along; his brightness at the background plane went 0.366 to 0.560. facing turned out to be inverted in the parking solve — he presented everything with his back to it — which is one character, confirmed on screen at both ends first. A presentation is now anchored to its element so he scrolls with it, and a Smash-Bros-style beacon with a live second render appears when he leaves the viewport. The card_stash fan was respecified as a computed layout in the plane the reader sees, dynamic to twelve cards; a polar version failed its own property test at five, which is the best argument I have for writing property tests. A Fable review then found four ship-blockers, all the phase machine and the objects a state deploys disagreeing under interruption — including a dead constant that was putting the reported jutter back at the outro boundary. 91 tests. |
| 2026-08-20 | Claude Opus 5 | Deck-E, real card art | The cards he handles now show the user's ACTUAL cards. cardArt.ts gives every card node its own material — Card_Front_Rose3 is one material shared by six meshes and their clones, so the obvious assignment puts the same card on all of them — and cardSource.ts is the one file in the character that knows the product's API exists. Card images carry a marker query parameter, because they come from a same-origin path that 302s cross-origin and every <img> in the app poisons that cache entry for WebGL: the same trap as the bug reporter's screenshots. The card back is the real Pokémon TCG back, composited over its own border navy so its transparent rounded corners do not render as four black notches. card_stash runs in BATCHES past twelve — a batch comes up, files in, and the top of the stack in his box becomes whichever card went in last — up to forty-eight, past which the rest are dropped loudly in notes[] back to the model. runCommands is async now, and queued per character, because naming a card is a catalog lookup and REJECT LOUDLY only works if an unresolvable id can be waited for. Review found three defects and all three were batching breaking a rule the old code already had: an unbounded eviction loop that could hang the tab, a new set of cards silently swallowed by the no-op guard while the call reported success, and — worst — interrupting a non-final batch freezing twelve cards rigid in mid-air for the length of the outro and then deleting them. closeProgress is pure and exported because of that last one; its property test is that the close never runs backwards. A fourth reported defect — the fan interpenetrating at twelve cards — turned out to be an artefact of measuring bounding boxes instead of cards, and tuning the constants to satisfy that proxy made the real separation worse before it was measured properly; the constants are unchanged and the test now measures the quads. 114 tests. Then the card back did not ship, twice: .gitignore and .vercelignore both carry a blanket *.webp for the image cache, both already had a marketing negation for the same reason, and fixing one left the asset in git and still absent from the deploy — a state visible from neither a local build nor CI nor git status. check-precache.mjs gained a second gate that fails the build when an asset the character fetches is missing from dist; it caught the second half within minutes of being written. |
| 2026-08-21 | Claude Opus 5 | @cheyras | Deck-E rev 2, PRs 1-4 and 7: fixed the browser tool wire bug (flyTo/goTo/highlight/scrollToMe had never once executed, a state field checked on the wrong object) plus a multi-leg journey loop; gave /api/chat server-side entitlement (DECKE_ENTITLED_USER_IDS) and a durable Postgres spend meter (migrations 039/040) where none existed before; extracted the MCP's 23-tool layer into a new packages/agent-tools package, proven behaviour-preserving three ways, now fronted by both the MCP adapter and a new AI SDK adapter that gives Deck-E the read half plus a lazy per-tool-call RLS context with a watchdog; and added a metered four-tool deep tier (plan_deck, analyze_collection, write_strategy_guide, research_meta — the last holding no tools at all) with maxDuration raised 60→300 for that reason alone. Synced AGENTS.md, ARCHITECTURE.md, SECURITY.md, README.md, apps/mcp/SPEC.md, and this wiki (Architecture, MCP-Setup, Decision-Log) to match. |
| 2026-08-22 | Claude Opus 5 | @cheyras | Deck-E, PR #74 (open, not merged, not deployed): fixed a blocking bug where the browser dropped the AI SDK's approval signature when replaying a tool-approval-response — with DECKE_APPROVAL_SECRET now set in Production and Preview, that made validateApprovedToolApprovals throw on every approved write, so "Go ahead" on the deployed preview wrote nothing while claiming consent had been captured. Verified against a live preview in both directions: signature present, the approved tool's execute runs; signature stripped, the leg dies after 2 chunks. This was the second defect in the same buried replay construction, both invisible because the round trip lived inside a React hook that does its own fetch and supabase.auth.getSession() and cannot be driven by node --test (it imports lib/supabase, which reads import.meta.env at module scope). Extracted it into two pure functions in apps/web/src/character/host/approval.ts with 10 tests, three of which drive the real convertToModelMessages from the pinned ai@7.0.66; both historical approval bugs were deliberately re-introduced to confirm the tests are load-bearing (bug 2 turns 3 red, bug 1 turns 2 red, including the real-SDK test each time). Also, from the same adversarial review: moved the narration stream transform from api/chat.mjs into apps/api/src/decke/narration.ts with 7 new tests, fixing an orphan text-delta flushed under a literal 'narration' id that no text-start ever opened (harmless to our own reader, dropped by a conformant one — the tail of a real sentence); search_cards on an unknown set now returns fail instead of ok, because grounding.observe was harvesting card-id-shaped tokens from every successful result, so a guessed set_id could ground itself and let sanitizeScreen wave through a card grid built on it; and corrected a stale justification comment in apps/api/src/decke/tools.ts (the .min(1) workaround cited a defect that is not live in the actual chat fallback, google/gemini-2.5-flash — the workaround stays, on the honest reason). Landed together in 610b183 (pre-rewrite SHA; resolves against deckpal-mirror-2026-08-31), 449 tests passing, every package building. Also found, and recorded in DECISIONS.md before being fixed: on gate 10 ("Add 4000 Charizards") the deployed preview reported "0 → 4000" with get_card, get_card as its entire tool log — no log_cards call at all — so the model fabricated a preview and reported it as done. Not yet fixed; investigation in flight. Shippable in the meantime only because the approval gate does not depend on him being truthful: a fabricated preview writes nothing, and a real write still cannot execute without a human approving it. |
| 2026-08-22 | Claude Opus 5 | @cheyras | Deck-E, decke-agent branch, commit 9ba77b0 (pre-rewrite SHA; resolves against deckpal-mirror-2026-08-31) (still open, not merged, not deployed): fixed gate 9's other blocker — asked to add one swsh4-162, Deck-E called get_card, stated the numbers ("Adding 1 Normal version would take you to 1. Sound good?"), and stopped; asked again with the card id, he called get_card again and asked again. log_cards calls: none, approval requests on the wire: none. Cause: prompt.ts's own write protocol ("Preview first ... before anything happens", "Confirm ... and wait") was duplicating the AI SDK's needsApproval gate — the only one of the path's three consent mechanisms that is real, and it only exists once the tool is actually called, so his careful preview-and-wait pre-empted the dialog that would have asked the reader. Four hypotheses were ruled out first, each measured (0/5, 0/5, 0/5, 0/15) before anything was rewritten, including one where more words made it worse. A first fix measured 22/30 but failed the deployed gate 0/2 — traced to the probe harness asking an easier question from / (5/6) instead of the /series page gate 9 actually opens on (2/6). Rewritten with that page as the target: 0/15 → 21/30 on the opening turn, 12/12 on gate 9's full three-turn script. Also confirmed NOT a regression from the 2026-08-22 chat-model switch: the old prompt scores 0/15 (grok-4.1-fast-non-reasoning) and 1/15 (grok-4.20-non-reasoning); the new prompt scores 9/10, 21/30 and 7/10 on those two plus the declared gemini-2.5-flash fallback — the defect and the fix both reproduce on every model tried. Verified on a live preview: gate 9 passes twice, a signed approval commits (ledger 10→11 and 11→12, card 0→1 both times), and the ledger does not move while the approval is held. Gate 11 (prompt injection through page data) still passes. Gate 10 remains red on the pre-existing alert_dizzy reaction defect, confirmed to fail identically on the pre-change build — not a write defect. Honest ceiling stated in the entry: roughly 30% of opening turns still stall and need a follow-up turn to recover; that is what a prompt-level fix buys, not full resolution. |
Nine notes from a screen recording, four with causes nowhere near where they were
seen. Boot's "frame skip" was the float modulation stepping 0 -> 1 while only the
pose was crossfaded (13.3x the idle floor in rendered pixels, now 1.8x). Travel
pace is now a playback scale on the solved track rather than a cruise speed, which
destabilises the solver — 45-47% of the old durations, and the change shipped a
worse bug for an hour when sampleTrack's frame-based index stopped agreeing with
the scaled duration and he teleported the last half of every leg. Both mobile
scale complaints were one cause: six viewport metrics, five of which ride iOS
Safari's 82px toolbar, read independently all over the runtime — now one module,
measured once from the canvas, which is pinned to 100svh. Overscroll cannot be
followed (every readable scroll metric is pinned flat through the bounce, measured
under a synthesised compositor gesture) so it is prevented instead. Card colour was
the AgX curve, not the lights: mean deltaE76 38.4 -> 8.8.
Full reasoning, measurements and the not-fixed list in Decision-Log.md.
Deck-E and his highlight ring are handed to the compositor while he is presenting:
both overlays are pinned into document flow at a document offset, so following the
page costs no frames. Kept the vertical-parallax cue by shifting the camera's
frustum instead of freezing his world position — the cue lives in the frustum, and
freezing him deleted it invisibly. Added pageAnchor.ts, ten pure tests covering
the containing-block measurement and the frustum-offset invariants, a PIN line in
the dev instrument, and ?present=<selector> so the phone can be set up from a URL.
Verified across Chromium, WebKit and Firefox, and on the owner's iPhone.
Measured foil detection against real photographs for the first time. The owner shot
three narrated 4K clips — three cards, both printings, three lighting conditions,
with sleeve finish deliberately crossed against variant so a classifier could not
score by detecting gloss. Ground truth came from his narration, transcribed on the
GPU whisper sidecar and then verified frame by frame. Built a harness that
rectifies each frame to canonical card space (card detection keys on FOCUS, not
brightness — Otsu on luminance merged card, hand and lit table into one blob whose
corners were not the card's) and measured five candidate features. corrLS reaches
AUC 0.988 with card and lighting held constant, but only 15/17 when the light
varies, with both errors confident rather than marginal — so auto-detection is not
shipped and the write-up records why, along with two dead ends and one
selection-bias bug that had quietly discarded the most informative frames.
Finished the rip flow instead. Added a per-row printing selector, since the scanner
matches artwork and cannot tell a card from its reverse holo even in principle.
Fixed a duplicate-cardId defect found while wiring Deck-E into the flow: a
re-shown card appended a second row that every consumer would then treat as the
first. Gave the screen palette a producer (showScreen) and corrected the turn-stop
condition it exposed. Verified the brain end to end against the live gateway across
six runs, checked both panels in a real browser at 390px and desktop, and
documented the assistant layer in ARCHITECTURE.md §15b.
| 2026-08-24 | Claude Fable 5 | @cheyras | The Deck-E animation pass (PR #81), both rounds: presence state machine, exits everywhere, dolly-station coupling, background resting depth, lean law (D8 closed), card spotlight for virtualized tiles, 2x snap chat legs, strand fixes; verified by 576 web + 237 api tests and the visual harness with vision-judge + pixel measurement |
| 2026-08-24 | Claude Opus 5 | @cheyras | Fixed Deck-E's mobile chat placement (PR #82): composerTop was measured against the viewport while the panel sat at translateY(100%) under its backwards entrance fill, latching to -670 and putting the park box off-screen — which both dropped him to the old low corner and disabled the transcript's right-edge clearing. Measured against the panel instead; reflow gains composerTop; the composer's left gutter deleted. Verified by WebGL colour-buffer measurement at 375x812, 576 web + 52 api tests |
| 2026-08-24 | Claude Opus 5 | @cheyras | Deck-E's iOS keyboard (PR #84, after #83 was abandoned): root cause was viewportToBlender assuming the canvas sits at client (0,0) — iOS slides every fixed layer when it reveals a focused input, the canvas measured -268, and he was drawn that far above his mark (the "disappears", and the exact 2x drift). Plus: the panel now fits above the keyboard rather than trusting iOS's unreliable reveal, the drag is refused with a non-passive touchmove instead of corrected after the fact, min-h-0 lets the short panel scroll instead of overflowing its own header, and auto-focus is desktop-only. Verified on real hardware: iOS 26.5 / iPhone 17 Pro and iOS 18.6 / iPhone 16 Pro |
| 2026-08-24 | Claude Opus 5 | @cheyras | Deck-E keyboard drift (PR #85): the scrollTo(0,0) pin was correcting a scroll iOS ANIMATES, so it fired against a moving target for the whole animation — a drift on a phone, invisible on the simulator. Now the scroll is absorbed with a compensating translateY(scrollY) on every fixed layer that rides it (panel, scrim, app header), so nothing writes to the scroll position and there is no feedback loop. Verified frame-by-frame on iOS 26.5 and 18.6 |
| 2026-08-24 | Claude Opus 5 | @cheyras | Reverted the Deck-E keyboard-fitting stack (PR #86) on Chey's call: kbInset, the scroll pin, the compensating transform, the touchmove refusal and the min-h-0/overscroll-contain support were six mechanisms deep, each load-bearing for the last, all to remove a scroll nobody minded — and the last one introduced a drift on send. Kept the canvas-origin projection fix, which is what actually fixed the reported bug. Verified after reverting on iOS 26.5 and 18.6 |
| 2026-08-24 | Claude Opus 5 | @cheyras | First-load blank gray screen (issue #75, PR #87): index.html preloaded the 1.2 MB Decke-runtime chunk on every page for every visitor — an advancedChunks group absorbs its matches' DEPENDENCIES, so cardSource.ts importing lib/api.ts pulled the app's shared plumbing into the character chunk and welded three.js to the entry. Claimed src/lib/** + src/character/*.ts into a higher-priority group, moved the three-free boundary modules out of decke/, added check-precache.mjs gate THREE for the document's critical path, and gave index.html an inline boot state. Cold first content 11.6 s -> 6.9 s, critical-path JS ~597 kB -> ~294 kB gzipped; verified at 1440px and 390px against the live backend |
| 2026-08-25 | Claude Opus 5 | @cheyras | Deck-E animation round two, from the owner's second narrated review (56 timestamped notes): his size stopped tracking the composer draft (1.281x to 0.995x, measured), the two-flights-per-commit race that stopped every hop dead (4/4 to 0/4 stalls), the entrance growing from the real chip, the facing turn bounded by its leg, the scroll race that left him diving off the page, the card mirror gate, the ring bleeding into the next tile, cardArt wired to the catalog, both budget failures telegraphed, and the expression doctrine rebalanced to per-beat; two new asserting probes (probe-decke-size, probe-decke-flight) with before/after controls; 598 web + 237 api tests |
| 2026-08-25 | Claude Opus 5 | @cheyras | Deck-E follow-up on the same day's pass: the entrance now grows DURING the hop (leaves the chip at 0.23 scale, full size 150ms after the leg starts, was 100ms before it); the arrival flinch traced to a SECOND flight per entrance from the composer mark-watch counting the panel's own animation, now one leg and a 4-5px settle instead of 8.8px plus a hop; and the dismissal keeps his depth plane so he no longer swells x1.45 on the way into the chip. New probe-decke-present.mjs covers the presentation round trip, the leg no probe covered — which is why the earlier pass reported that pair fixed without exercising it. Three false-PASS flaws in my own probes found, fixed and written up. 598 web + 237 api tests |
| 2026-08-26 | Claude Opus 5 | @cheyras | Card art: measured the reported slowness before changing anything (p50 1954ms / p90 4154ms / slowest 12.6s, 7 of 22 tiles blank after a 6s settle, 100% of image requests were 302s), then fixed three separate causes — the SPA now addresses the public object URL directly via a new @deckpal/storage/paths subpath export (image tier kept as the self-healing fallback, crossorigin so the SW stops caching quota-padded opaque responses, deckpal-img-v1 retired on activate); warm:cloud added as the cloud tier's missing bulk warm path, which found and filled 18,840 of 21,066 cards that had no object at all; and a card with no manifest row can now self-heal from its canonical derivation instead of serving a placeholder forever. Also killed a duplicated 1,025-species request on every /pokedex load. Live production after the warm: p50 1954→338ms, slowest 12,647→626ms, /pokedex sprites 37/43→100/100. Honest residue: ~592 cards (2.8%) have no upstream source. 6 new cardArt tests + 12 web / 63 api / 8 images / 11 storage / 33 paths passing |
| 2026-08-27 | Claude Opus 5 | @cheyras | Deck-E narration leak filter (issue #90): TOOL_TAGS was a hand-written seven while buildTools exposed nine, so a model that wrote <journey> or <xai:escort> as prose reached the reader unfiltered -- with no type error and no failing test to say so. The alternation is DERIVED from a new COSMETIC_TOOLS export now (SERVER_TOOLS + CLIENT_TOOLS, both pinned against whether the tool has an execute), and narration.test.ts reads the tool registry itself to assert every name is stripped as an element, behind a namespace prefix and as a name="..." attribute. Drift guard proved by injecting an unlisted tool: 4 tests fail, one saying "wave leaked as an element". The 23 data + 4 deep tools stay out of scope on purpose (ordinary-English names vs. an attribute rule that strips whole elements) and the boundary is written down. 6 new narration tests + 1 registry pin; full CI-equivalent green, test:decke 316 -> 323 |
| 2026-08-27 | Claude Opus 5 | @cheyras | GLC set carve-outs (issue #93): the Celebrations Classic Collection deny_except rule was vendored, typed and loaded but no validator read it — and because every Classic Collection card is a fingerprint-identical reprint, the reprint oracle waved the whole set through the GLC pool rule, so an illegal deck came back legal. Implemented ruleSetCarveouts as a hard deny that outranks the oracle (§2.3.6 item 5 subtracts carve-outs from the pool), keyed on set id + normalized name, with the pool rule suppressed for carved-out cards so one illegal card yields one row. Four fixtures incl. the mirror-image false-positive guard and a set-id drift pin; test:deck 68/68 and full CI-equivalent green |
| 2026-08-27 | Claude Opus 5 | @cheyras | Deck-E prompt revision (issue #91): the "Rules, in the order they matter" list in decke/prompt.ts numbered two consecutive rules 3. — 1,2,3,3,4,5 — telling the model every turn that two rules share a rank and that there are five rules where there are six. It had been deliberately left undone because that file documents wording AND paragraph position as measured artifacts, so three bytes of list marker is a prompt revision, not a typo fix. Renumbered 3/3/4/5 → 3/4/5/6 with nothing else moved, and measured it: decke-tool-choice-probe against MODELS.chat from route / with gate 3's own sentence, n=20 per arm, dist rebuilt between arms — looked-something-up 20/20 → 20/20, questioned-the-set-exists 0/20 → 0/20, invented-a-card-count 0/20 → 0/20. Then ran the browser gates too, against the preview built from the change: 3, 4, 9, 13, 14, 20, 23 PASS and 10 on its own documented SKIP. Gate 21 went red and got a control rather than a conclusion — 9 runs each, this change 4/9 vs main 5/9, i.e. a pre-existing ~50% flake, recorded and deliberately not fixed here. New test pins every ordered list in every prompt branch to 1..n; both new assertions fail before the fix. test:decke 318/318, full CI-equivalent green |
| 2026-08-27 | Claude Opus 5 | @cheyras | PDF export brand mark (issue #92): header() drew the literal 'pokédex', so every deck/list/set-checklist export shipped a name two renames old (pokédex -> DeckScout -> DeckPal) on the same page whose footer stamp and Author already said DeckPal. The brand was five literals in one file; the renames swept four and missed the one nothing renders in review. One BRAND constant now. Swept the repo and classified every surviving occurrence: the /pokedex route, pokedex_binder kind, 'Pokédex binder' label and 'Pokédex' species set-id are the FEATURE and were left alone (an earlier sweep of exactly this kind once 404'd every species page). Verified by rendering real PDFs -- pdftotext diff is one line per document, and the pages were rasterised via Windows.Data.Pdf and eyeballed. New pure test:export suite reads the strings back out of the PDF bytes (inflate + rejoin pdfkit's kerned TJ runs) rather than trusting the module's own constant: 6 of 9 fail pre-fix, 9/9 after, wired into ci.yml as its own step. Also fixed the stale .brand-wordmark comment in theme.css. Full CI-equivalent green |
| 2026-08-27 | Claude Opus 5 | @cheyras | Tool-error redaction (issue #94): log_cards formatted the raw (err as Error).message into its result text while its planBatch resolves cards over SQL — so a pg failure printed password authentication failed for user "deckpal_prod" into a model's context, and over MCP into a third-party provider's. Audited every catch in packages/agent-tools: 16 tools, 21 sites, of which log_cards and edit_list are demonstrably SQL-backed. All of them route through errText() now, because "route the SQL-backed ones" is a call-graph question that was already answered wrongly once. errText itself had a gap — it reduced a driver error to its SQLSTATE only when the error carried a code, while its comment claimed otherwise — so its fallback now scrubs DSNs, IPv4 addresses, host:port pairs and for user "…" while keeping our own readable messages intact. New toolErrors.test.ts runs the real handlers against a ctx.db that throws real pg error shapes (no DB, contract B7) and adds a source guard; 5 of its 10 tests fail pre-fix. Also removed a literal NUL byte from tools/logging.ts that made git and grep treat the file as binary — no reviewable diff, in the file that most needed one — with the hash input unchanged. agent-tools 61 → 71 tests; full CI-equivalent green. PR #119 |
| 2026-08-27 | Claude Opus 5 | @cheyras | Purchase Set cart links (issues #89 + #113, confirmed the same bug): PurchaseSetMenu hand-rolled its fetch and wrote the self-host prefix /deckpal/api/..., which cloud has no rewrite for -- so the catch-all SPA rule answered with 200 text/html, res.ok passed, and the failure landed in res.json(). On WebKit that rejection's entire text is "The string did not match the expected pattern." (bare ExceptionCode::SyntaxError, no message of its own), which is verbatim what the iPad reporter of #113 saw on /series/mega-evolution/me05 -- the page this component lives on. Fixed at the client layer: new api.setMassEntry() brings the base path, the missing Authorization header (the route 401s without one, so fixing only the string would have swapped a mystery for a 401) and the 401-refresh retry. Added two guards: a content-type check that turns a non-JSON 2xx into a message naming the path, and check-api-base.mjs, first step of the web build, which fails on any hardcoded API base in apps/web/src (fails on the pre-fix tree, passes after). Verified in a real browser against the live backend as the QA account, desktop + 390px, before and after. Reported not fixed: proven vs best-effort cart URLs are rendered identically in the UI, which is closed issue #37's symptom; the 20s timeout is not a cause (measured 2.1-2.9 s, ~7x headroom). Full CI-equivalent green |
| 2026-08-27 | Claude Opus 5 | @cheyras | First-paint deadline on the auth session (issue #75, second pass): #87 fixed the modulepreload regression and the grey screen kept happening, because the actual hang was supabase.auth.getSession() -- which refreshes over the network inside the 90s expiry margin, with no AbortSignal or timeout anywhere in auth-js's request path. main.tsx's index beforeLoad, AuthGuard and api.ts's authHeaders() (before EVERY request) all awaited it, so a stalled socket left #root with 0 children indefinitely -- and #87's inline "Loading DeckPal" state could not cover it, because React's first commit had already replaced it. Shipped lib/sessionDeadline.ts (pure, unit-tested) + lib/authSession.ts as the single choke point, converted 8 call sites with non-destructive fallbacks (a timeout is UNKNOWN, never "signed out"), a build gate that fails on any raw auth.getSession()/auth.refreshSession() elsewhere, and an inline 12s first-paint watchdog with a Reload button for every other cause. New Playwright probe holds the token endpoint open and asserts the property: 4/4 blank before, 4/4 rendered after, exit 1 -> 0. 10 new pure tests; ruled out the service worker, navigator.locks and IndexedDB with evidence. Full CI-equivalent green |
| 2026-08-27 | Claude Opus 5 | @cheyras | SSRF hardening (issue #96): six critical js/request-forgery alerts open and untriaged since 2026-08-14, three distinct findings. The serious one was fetch-source.ts — an arbitrary URL with redirect: 'follow' and no host check, in the image function that holds SUPABASE_SERVICE_ROLE_KEY and republishes whatever comes back to the PUBLIC card-art bucket at a derivable path; follow is what made it reachable from a hostile/compromised upstream with no DB write. Now an explicit upstream allow-list (upstream.ts: assets.tcgdex.net, raw.githubusercontent.com — traced every writer of image_asset.source_url and card_set.logo_url/symbol_url to establish that set) enforced with redirect: 'manual', re-checked on EVERY hop, relative Location resolved, 5-hop cap, one AbortSignal across the chain, plus a resolved-address screen for loopback/RFC1918/CGNAT/169.254.169.254 in v4 and v6 including the mapped/NAT64/6to4 shapes. Content checks (content-type, magic bytes, size) untouched — body vs destination are complementary. assertSafeObjectPath now guards every exported Storage function that takes an object key, using the same regex parseImagePath uses, and THROWS rather than answering "not found" (objectExists/headObject catch-to-miss would have swallowed it). agent-tools/src/api.ts composes through new URL() with a scheme/host/prefix assertion and the call sites percent-encode ids — which also surfaced a latent /lists/null. assets.pkmn.gg deliberately left OFF the allow-list (≈58 rows point there; ruled out on legal grounds 2026-08-26) — flagged for the maintainer in DECISIONS.md, one line to reverse. 60 new assertions; the redirect case runs two real loopback servers with the non-allow-listed one's hit counter asserted at zero, so a test that only checked the initial URL could not pass. 49/60 storage assertions fail pre-fix; agent-tools suite does not import pre-fix. CodeQL cannot be run locally, so it was confirmed on the PR: the first push closed five alerts and re-raised the fetch site as #63 — a fair call, since the checker returned the caller's URL rather than one built from the allow-list. Second push selects a CONSTANT origin per allow-listed host and rebuilds the request from it, keeping only a decode-checked path. refs/pull/123/merge now carries ZERO open code-scanning alerts of any kind, no dismissals (B9). Full CI-equivalent green. PR #123 |
| 2026-08-28 | Claude Opus 5 | @cheyras | Deck-E mobile pass, from a 5m56s iPhone screen recording (transcribed via the Vercel AI Gateway, keyframes read at 4s and the mascot tracked at 10 Hz). FIVE defects, four fixed with a before/after control run in the same sitting. (1) He stood ON the permission prompt -- his park box anchors to the composer and the approval card is a sibling above it, so his head covered 'the deep research takes longer and uses more than a normal request'. New parkFloor.ts takes the higher edge with a ceiling (tall card) and a floor of composerTop (mis-measured panel); new probe-approval-clearance.mjs measures both boxes at 390x844 on the real app: 101px overlap -> 8px clearance, desktop unaffected (he parks outboard of the column). (2) The slow downward drift and jitter -- tracked frame by frame, six discrete hops over ~90 CSS px in 4.9s, i.e. the mark watch firing six times because any move over 1px bought a full flyTo. A size threshold alone got it to four; the missing idea is that you cannot fly to a moving target, so MARK_QUIET_MS gates the flight on the mark having been let alone first. probe-park-settle.mjs replays the tape's own six hops: main 6 flights -> 1. Plus a 3px height deadband to break the height->gutter->rewrap->mark-moved loop that generated them. (3) express telegraphed as 'Change how he looks - applied 1 command(s)' against the tool's own documented contract; the server stops emitting and the client refuses to draw, both pinned, control run shows the row before and the decks row still drawing after. (4) He proposed a WRITE when asked to read -- new decke-read-vs-write-probe.mjs reproduces it on the live model against the real prompt, and n=44 showed eight proposed guide-writes, ALL EIGHT byte-identical to the guide just read. So noOp.ts answers 'would this change anything?' at the approval boundary, consulted from both needsApproval and execute (needsApproval:false means no dialog, never 'run it'), failing open to ASK on every path that cannot answer. (5) Lookups on a pure-feedback message: a prompt rule, and the one change here with no measurement -- the probe could not reproduce it cold (0/12 both variants), which is recorded rather than papered over. The prompt paragraph for (4) is likewise credited with nothing: 2/12 before, 2/12 after. 22 new tests (parkFloor 8, markWatch 6, composerRuler 5, noOp 14, legWiring 1) and three new browser probes, each carrying a control. Full CI-equivalent green: web 644, api decke 345, api deck 72, agent-tools 93, typecheck clean |
| 2026-08-28 | Claude Opus 5 | @cheyras | Post-keyboard chat-bar drift (follow-up to the same day's Deck-E pass, which fixed the WRONG LAYER). That pass read the drift as Deck-E re-parking six times -- real, still fixed, and not what was reported: the thing that drifts is the PANEL, and he was tracking a composer that is itself sliding. The panel is fixed; bottom: 0, iOS does not shrink the layout viewport for a keyboard, and WebKit scrolls the held document to reveal the focused input (it does this even under overflow: hidden), so every fixed layer rides that scroll -- then rides the ANIMATED unwind back down after the keyboard's own animation ends. character/viewport.ts already carried the iPhone measurement (canvas 0..760 down, -268..492 up) from an unrelated bug and nothing had connected it to this. New keyboardInset.ts lifts the panel's own floor by the measured keyboard height and pins the document at scrollY 0, so there is nothing to reveal and therefore no unwind -- the mechanism is removed rather than damped. The two halves are inseparable by construction (shouldPinScroll refuses unless the inset is applied; a property test asserts no sample can split them), every unreadable viewport returns 0 which is the old layout byte for byte, and the pin gives up after three attempts that do not move scrollY so a no-op cannot leave the panel lifted twice. New probe-keyboard-settle.mjs fakes visualViewport on the real object and dispatches its real event, with a control: bar lifts 0px -> 268px for a 268px keyboard, his mark comes with it, 0px of drift across 2s. Stated in the probe header and DECISIONS.md that WebKit's reveal-scroll itself is NOT reproducible headless and is not claimed to be tested -- the arithmetic is pinned against the hardware numbers instead. Desktop asserted inert (0px for a faked keyboard). 10 new unit tests; web 644 -> 654; the three probes from the earlier pass re-run and still pass, on production. PR #129 |
| 2026-08-28 | Claude Opus 5 | @cheyras | Post-keyboard chat-bar drift, third and final attempt (#131), after #129 was REVERTED (#130) as worse on the device. The two previous attempts both described a KEYBOARD; the thing that moves is a fixed LAYER. iOS does not shrink the layout viewport for the keyboard -- it scrolls the document to reveal the focused input and lets fixed layers ride that scroll, so the panel lands on the keyboard by accident and stays there only until the reader flicks. Measured on iPhone 17 Pro / iOS 26.5 with the instrument now checked in as KbDiag.tsx (gated behind ?kbdiag=1, deliberately shipped because no headless browser has a software keyboard and reasoning from the outside had already shipped two wrong fixes): at rest scrollY 1 / vh 714 / panel bottom 714; composer focused 338 / 377 / 377; ONE FLICK LATER 436 / 377 / 279. The keyboard never moved -- the composer went 98px up the screen. The finding that settled the design: with a rule drawn on the glass at each candidate coordinate, once the document has scrolled past iOS's own reveal BOTH visualViewport.height AND window.innerHeight under-report the visible area by exactly the extra scroll, so there is no number left on the platform that says where the keyboard is (cf. Apple Developer Forums 800125 / WebKit #297779). The scroll therefore has to not happen. Shipped panelViewport.ts (places the panel by writing top+height from two measurements taken off the app header, so it covers the visible area rather than being offset by a keyboard-shaped number) and panelScrollLock.ts (refuses a one-finger drag inside the panel while the composer has focus when nothing between the touch and the panel can absorb it; a transcript with messages still scrolls). #129 was right that the scroll is the mechanism and wrong to PIN window.scrollY against WebKit every frame -- nothing here writes or pins a scroll position, a gesture is refused with preventDefault as every scroll-locking drawer does, and WebKit's own reveal is programmatic and unaffected. Rejected with measurements: the one-line bottom = innerHeight - visualHeight - offsetTop formula the web repeats to each other (on iOS window.innerHeight tracks the VISUAL viewport, so it read -268 in the state it was written to leave alone -- it never fired in the case it existed for, and only its own sanity clamp stopped it moving anything); interactive-widget=resizes-content (unimplemented in Safari, WebKit #259770); navigator.virtualKeyboard (Chromium only); overscroll-behavior: contain alone (measured: the document still scrolled to 445 -- Safari does not honour it for chaining to the document; the class stays as correct intent, the lock is the guarantee). Same-day follow-up from the owner's own film of the fixed build: first keyboard-up clean, second had him slide too far and hop back -- read frame by frame at 60fps it was a RACE, not a bad number. iOS animates the visual viewport up over ~200ms, and DeckE marked his station dirty on document scroll/resize only -- neither fires during that animation, and update()'s own comment claimed a visualViewport listener that had never existed. He now listens to visualViewport resize/scroll and TRACKS rather than flies, and the panel's geometry is written as two CSS custom properties in the event handler so it lands in the same frame as the viewport it was measured from. Also in the same branch, on the owner's ruling that "hop is really for when he is PURPOSELY traveling somewhere... not intended to be used for tiny page shifts": launch, the choke point every flight passes through, now asks worthHopping() -- above the threshold nothing changes, below it the same solved eased track is flown flat (arc: 0, bow: 0). Piloted on real hardware before merge, both installed runtimes -- iPhone 17 Pro / iOS 26.5 and iPhone 16 Pro / iOS 18.6 -- keyboard up and down, scrolled both directions, dismissed; filmed at 60fps and read frame by frame: he is at his final position on the first frame and holds it for the whole slide. Reported NOT verified on device: a long transcript scrolling under the lock (this machine's dev server could not reach the live backend, so no conversation could be created; absorbs is unit-tested and the DOM walk around it is 15 lines), and a small re-park sliding rather than hopping (not reproducible on demand). Full CI-equivalent green. PR #131 |
| 2026-08-29 | Claude Opus 5 | @cheyras | Card marks and card geometry, from a screen recording, shipped as four PRs (#134 rarity, #135 geometry, #136 hover, #137 set assets). Run through Ringer: two read-only research rounds, then implementation lanes with disjoint file ownership in one shared checkout, each gated by an orchestrator-authored contract test the implementing agent could not edit. Rarity — rarityGlyph() collapsed 30 catalog values onto five Unicode characters; replaced with authored SVG per rarity. Built first from a written source, which was WRONG, and corrected only after fetching one real high-resolution scan per rarity and reading the print: Hyper rare prints three gold stars (jtg/188), and six rarities (Holo Rare VSTAR, Rare PRIME, LEGEND, Radiant Rare, Full Art Trainer, Shiny rare V) print a plain star where the first pass had invented a letter badge — the same defect the owner had complained about for set symbols. Every table entry now cites the card it was read from. Promo, Classic Collection, Black White Rare left UNVERIFIED. No Pokémon rarity artwork is cleanly licensed (Malie's licence section reads "FIXME"; Bulbagarden's files carry only an uploader fair-use claim), so the geometry is ours. Geometry — 63 × 88 mm with a 3 mm corner, so aspect-ratio: 63 / 88 and a radius of 4.7619 % of width, both COMPUTED from the millimetre constants; replaces 245 / 337 (1.55 % too wide) and a flat 8px. GridView's virtualisation ratio derives from the same constant. Radius triangulated, range 2.5–3.0 mm, stated honestly rather than implied as official. Hover — the lift was on the <img> inside CardImage's overflow-hidden frame, so the card grew into its own clip; moved to the frame (.px-card-art), measured 241.81 → 245.20 px uncut. The old a:hover img was unscoped and also moved logos, sprites, avatars and brand marks; those are now still by design. Set assets — 90 missing (set, symbol/logo) pairs, all with NULL catalog URLs so the warmer never tried. A static crosswalk fills 43: our ids are zero-padded and dotted where the source's are not (me02→me2, sv08.5→sv8pt5), and subsets serve their parent's symbol under their own id (swsh12tg/symbol.png is byte-identical to swsh12's). 47 stay blank deliberately — 12 McDonald's logos (byte-identical corporate mark, upholding the 2026-08-10 ruling), 20 Trainer Kit logos (one generic wordmark shared by four sets), and genuine upstream gaps. Code and tests only; no warm was run. Two process lessons banked: ringer's codex engine confines writes to the task dir, so an implementation lane pointed at a repo outside it silently produces zero edits (fix: sandbox_workspace_write.writable_roots); and vite served a stale transform cache, so a screenshot showed a feature working that was not running — caught only by querying the DOM. Verified: 717 web + 247 storage tests, typecheck and builds clean, browser at desktop and 390px, all four PRs green. |
| 2026-08-29 | Claude Fable 5 | @cheyras | The Deck-E agentic pass, from a narrated brief, on branch feat/decke-agentic-pass (6 commits, PR pending). Run through Ringer on GLM-5.2 (Codex was rate-limited; wired the opencode engine with a new bwrap sandbox wrapper for WSL). Recon: 13 unique claude.ai-via-MCP transcripts, the prior experience-pass record, a DECISIONS/git-history recurrence mine, and industry research. Shipped: get_card full rules text (the Lucky-Helmet class of from-memory advice loses its excuse); battle-log deck inference as a pure read (POST /decks/log-preview + optional deck_id) with the approval card as the confirm surface; dry_run on add_battle_log/edit_battle_log; explicit-args-beat-parser and a format-drift tripwire; printed set-code aliases (PAL/TEF/MEG/...) plus set_cart finally routed through the resolvers; suggest-once guide etiquette (name-level declines, extended to research_meta); research-backed guides (findings channel, DATA-framed after the security review caught it bridging the read/write split, no_research on the approval card after the correctness review found the restatement dead — wrong SHAPE keys); a prompt revision (UNPROBED, gate runs owed); and turn guards for the five failure modes mined from the owner's own 28 Deck-E conversations (empty answers in 13 of 28, phantom actions, flailing, truncation, ungrounded ids) — guard notes rewritten reader-facing after orchestrator review caught them addressed to the model. Verified: typecheck clean, every CI suite green (~1,760 tests), serverless functions load. Owed: live-DB get_card exercise and WS8 probe runs on a deployed preview. |
| 2026-08-29 | Claude Opus 5 (+ Fable reviewer) | @cheyras | Merged the card-marks work: #135 geometry, #136 hover, #134 rarity marks, #137 set imagery, in that order, then filled the image bucket. An independent Fable subagent reviewed all four before merge and found two blockers that would otherwise have shipped. (1) The set-imagery feature was inert on the cloud tier. IMAGE_SOURCE_POLICY allow-lists only the hosts a CARD path can derive (assets.tcgdex.net, raw.githubusercontent.com); 49 of the 50 crosswalk entries live on images.pokemontcg.io and archives.bulbagarden.net, so the handler resolved each URL and was then refused by its own SSRF control -- the THIRD failure of exactly that class in this feature, and invisible to a suite that only exercised the pure resolver. Fixed with SET_IMAGE_FALLBACK_POLICY: a separate policy used only on the set-fallback path, hosts DERIVED from the table so they cannot drift, gated additionally by isSetImageFallbackUrl() exact-URL membership. Strictly TIGHTER than the default (it does not even allow raw.githubusercontent.com), not a widening. setImageFallbackPolicy.test.ts executes the real upstream check against the real table. (2) Rules-of-Hooks violation: RarityMark called useId() after a conditional early return, so an instance whose rarity crossed the none/non-none boundary rendered a different hook count between passes -- React #310, reachable in production because CardDetail keeps its tab body mounted across ?card= changes, 'None' is a real catalog value, and the app has no route-level error boundary. One-line hoist. Also fixed from the review: header said 43 where the table holds 50; a residue paragraph was truncated mid-sentence and listed pairs the PR fills; image-slots.md named seven filled pairs as blank; hasSymbol had become a dead prop (removed at five call sites); the shoelace test re-implemented the formula instead of calling it (polygonArea now exported and checked against hand-computed values); the rarity mark was announced twice where its name is already visible (new decorative prop); Scan.tsx kept a second copy of 63/88 beside the token that now owns it. The reviewer gave a clean bill on the claims most worth doubting: the per-axis percentage radius really resolves to one circular corner, GridView's virtualiser cannot disagree with what CardImage paints, the optical scale is genuinely derived and applied about the glyph centre, and B1 provenance is correct at both choke points. Main moved twice mid-flight (#133, #139); every branch was rebased with a real merge in BinderView/CardTile. Final fill: warm:cloud --assets sets -- 133 filled, 224 already present, 41 failed, of which 40 are the documented residue and 1 (bw7 logo) was transient and now serves. All 50 crosswalk assets verified live. |
| 2026-08-29 | Claude Opus 5 | @cheyras | Price history tiered retention, from roadmap/plans/price-retention-tiers.md, on branch prices/retention-tiers. Daily price rows forever are ~6.6 GB/year against an 8 GB allowance, so history is now daily for ~30 days, weekly OHLC for ~6 months and monthly OHLC forever (migration 048 price_bucket, apps/sync/src/prices/rollup.ts, price-rollup.yml). Close alone misleads 46.8% of the time over 633,431 real weekly buckets, which is why a bucket and not a close; corr(stddev, high-low) = 0.9878, which is why volatility is derived and never stored; TCGCSV has no volume, which is why VWAP can never exist. The job verifies before it deletes — recompute-and-EXCEPT both directions, a conservation count no unbucketed series can hide from, a no-shrink check, then DETACH, then DROP one run later after re-deriving from the retired table. Also: the archive replay now treats a bucketed day as ingested (a replay would otherwise rebuild the partition the rollup just retired, greenly); snapshot-backfill's staleness gate became per-tier (2/9/33 days) or it would skip every day past the daily window; the reader serves three tiers as one shape; the Price tab draws close + a low-high band with a grain caption; and the agent licensing contract (what high_on/low_on permit, and what the path between them does not) is in the endpoint JSDoc and API.md, to ship verbatim in any future price-history tool. Verification: this machine has no Postgres, no Docker and no psql, so the plan's live-DB gates could not be run. Instead the whole pipeline ran against a real Postgres 18 engine (PGlite/WASM) — migration 048 verbatim, ~1,500 synthetic observations across 24 months, the shipped runRollup, the shipped reader SQL extracted from cards.ts so it cannot drift, and the shipped backfillValuePoints — with bucket values checked against an independent JS computation rather than the SQL that produced them. That found three real defects invisible to typecheck and to the pure tests: a parameter/predicate mismatch that made every month past the weekly band fail on the wire, a $1/$3 parameter gap in dropRetired, and a reader seam that drew a whole month twice. Typecheck, all pure suites (104 deck + 36 sync + 83 web) and every build clean. Owed to a human: the live catch-up run, the live endpoint and snapshot-backfill checks, the browser pass on the QA account, and the before/after pg_total_relation_size totals. The workflow ships with its cron commented out for exactly that reason. Then reviewed by a fresh Fable subagent with the same harness and told to break it — it found eight defects, all fixed before commit. Two mattered: price_bucket shipped with no RLS (the only table since 021 without it, on a table the rollup makes the only copy of its data, reachable by the anon role the public price endpoint runs as — now enabled on the parent AND every partition, since Postgres does not apply a parent's policies to a partition reached by name); and the rollup could bury an un-repaired ingest gap permanently and then close the repair path, because every check compares buckets to the partition (blind to days never ingested) while backfill treated any bucketed day as done — demonstrated end to end with the 2026-08-08 outage's shape. Fixed by refusing a month with un-ingested days (--allow-gaps to override) and by counting a day as covered only when some series' n_obs equals its bucket's full span. Also: a straddle-skipped month left a months-long chart hole (a refusal now HALTS the run, since rolling past it moves day_floor over rows only served below it); assertStraddleCoverage checked a partition's existence rather than its contents; --limit=0 meant 3; a comment claimed a name assertion that did not exist (it does now — those names are interpolated into DETACH/RENAME); the no-shrink check was vacuous on the drop path; and two resumability gaps (an orphaned partition after a kill between DETACH and RENAME, an unreachable FINALIZE) are now adopted on the way in. The reviewer independently confirmed B4, straddle ownership, the OHLC arithmetic, the absence of any vacuous pass at rollup time, every reader edge case, and no Insights regression. Lesson banked: the author's harness proved everything the author thought to doubt, and 1,100+ green tests said nothing about a missing RLS line. |
| 2026-08-29 | Claude Fable 5 | @cheyras | Walkthrough deferred items: server-side settings (049, PR #140), list cover mosaic (PR #142), smart lists (050, PR #143), variant-scoped decks (051, PR #144) |
| 2026-08-29 | Claude Fable 5 | @cheyras | Deck-E reliability pass (PR #146): pool double-checkout 500s, cross-turn failing-tool breaker, unplugged #138 guards wired + pinned, read/write approval split. Then an adversarial review round on the same branch: the breaker as shipped counted a tool's failures for the whole conversation and never closed on recovery — it would have refused decks for the rest of a conversation it had already recovered in — so failures are now counted SINCE THE TOOL'S LAST SUCCESS; and decke/toldAlready.ts annotates a read whose one-line summary the reader has already been shown (the transcript's other repetition, which succeeded every turn and so no failure-based guard could reach), sharing failing.ts's block parser so recovery and repetition cannot disagree about what a turn recorded. |
| 2026-08-30 | Claude Opus 5 | @cheyras | Shipped the price retention tiers to production (PR #141, then #145). Migration 048 applied to the live Supabase project; the two-year catch-up run under supervision — 23 months oldest-first, every verification exact (storedNotRecomputed/recomputedNotStored/shrunk all 0, conservation equal on every month), 2.374 GiB → 0.495 GiB, 19.26M daily rows → 1.74M plus 601k month and 716k week buckets. Preconditions checked rather than assumed: the archive backfill covers 2024-08-29 → 2026-08-29 with zero days carrying no observation. One month (2024-08) rolled with --allow-gaps because the backfill window starts on the 29th, so its first 28 days are absent by choice; its bucket honestly records n_obs: 3. Production surfaced two defects no local harness could. (1) Supabase ships statement_timeout = 2min on the database role, and the recompute-and-EXCEPT verification over a week-grain month (~24k variants × 5 weeks) exceeds it — the catch-up died on 2025-11 after writing its buckets and before detaching anything, the safe half of the failure and exactly what the ordering was designed for; re-running resumed. The job now raises its own timeout to 30 minutes (finite, not 0 — a statement stuck that long holds locks against the price ingest). (2) A response-shape change breaks the clients already running, and this is a PWA: "API and web ship in one commit" is necessary and not sufficient, because the browser keeps the previous bundle until reload and the SW caches API GETs for seven days. The old chart got points with no date, Date.parse(undefined) → NaN, and isoOfDay threw a RangeError that unmounted the entire card page behind "Something went wrong!" — caught by the browser gate, first click, on production. Fixed as a class: ValueChart drops any point it cannot place, chartPoints skips old-shaped points (the reverse skew), and the SW API cache is bumped to deckpal-api-v2 with a comment saying to bump it on every shape change. Verified on production: the endpoint returns 17 month + 23 week + 60 day points for 2y; the Price tab checked in a real browser at desktop and 390px showing monthly bands → weekly bands → daily line with the caption "daily since Jul 1 · weekly to Jan · monthly before", zero console errors. Monthly cron armed (3rd, 04:20 UTC) now the first run has been supervised. |
| 2026-08-29 | Claude Opus 5 | @cheyras | Root-caused and hardened prices-cardmarket on branch fix/cardmarket-observations (uncommitted). Reported symptom: price_current full of EUR, price_observation with zero source_code = 2 rows ever, five nightly sync_run rows all ok / rows_written: 26738. Both tables are written from one array in one transaction, so the evidence contradicted itself — so the write path was driven, not read, against a real Postgres (PGlite/PG18) over migration 007 verbatim plus 021 RLS, at production shape (19,865 products, 26,486 priced variants, 67 chunk boundaries, a +0200 stamp, a non-UTC session TimeZone). It appends every EUR row correctly, with the right source id, currency, captured_at, reverse-holo routing and NULLs, and killed all six listed hypotheses. rows_written was appendObservations’ own return value, so it asserted 26,738 committed history rows; and its equality with the price_current count is not evidence, since under a working implementation both are points.filter(hasAnyMetric).length. The finding is the reporting: a job that reports its intentions rather than its results cannot be used as evidence about itself. Fixed: a post-COMMIT read-back that makes rows_written a measurement and fails the run (failed, not partial — lastOkStamp would read partial as a claimed stamp and never retry) when the history is short of the priced points; a skipped sync_run row so a quiet upstream and a dead scheduler stop looking identical; and the partition/lookup work moved inside the error handler, where a throw used to leave status='running' forever and sync_run_one_active then failed every later startRun — one network blip, job permanently dead, silently. Nine pure tests in __tests__/cardmarket.test.ts (five fail against the previous code), 45 sync pure tests, three typechecks clean, two PGlite probes green. Owed to a human: four read-only prod queries to settle whether those rows were committed and later removed or the two facts came from different databases — B9, nothing was run against production. Flagged and not fixed: captured_at is not truncated to the source day, so Cardmarket’s +0200 stamp files every price on the previous UTC day and every 1st-of-month file into the previous month’s partition; ensureObservationPartition writes TZ-dependent partition bounds (measured FROM ('2026-08-01 00:00:00-06') on a Denver session); and price-refresh.yml’s Cardmarket step ignores its own force input. |
| 2026-08-31 | Claude Fable 5 | @cheyras | Project Holo 2a, archive half. Took the pre-deletion archive of the foil track and wrote its history down before it goes. Two offline archives outside every git tree at E:/Users/cheyr/holo-archive/: a git clone --mirror of cheyras/deckpal (deckpal-mirror-2026-08-31.git, 68 MB, git fsck clean with zero dangling objects, all eight foil/* tips plus main verified identical to origin by git ls-remote at clone time), and the research/foil-video-reference/ media corpus extracted from that mirror (foil-video-reference-archive-2026-08-31/, 523 files / 12 MB, the only copy that exists -- three of its six source videos are 2020-2022 and may not be re-fetchable). MANIFEST.sha256 covers all 387 binaries (344 .jpg + 43 .webm), verifies with sha256sum -c, and every one was additionally checked byte-for-byte against its git blob hash in the mirror -- 387/387 identical. Cross-checked against git ls-tree -r foil/tooling-research (523 tracked paths, 387 binary). No tags were created, deliberately: a tag keeps the objects reachable, which is exactly what the deletion is meant to end, so the mirror is the archive. Then the new Foil Branch Log page: 465 commits / 76,291 words across all eight branches, 262 of them with bodies over ~100 words reproduced in full and chronological within branch, the other 203 in a compact SHA/date/subject table -- nothing filtered. Verified programmatically: all 465 short SHAs present, all 262 long bodies byte-identical to %B, all nine tip SHAs recorded. One finding worth carrying: the eight branches are disjoint -- the per-branch --not main counts sum to exactly 465, so each is an independently rebased line and shared work appears once per branch under a different SHA. Repo untouched; this session made no commits to cheyras/deckpal. |
| 2026-08-31 | Claude Opus 5 | @cheyras | Project Holo 2c, part 1 (PR #150): retired the pkmn.gg reference surface from the working tree, six categories of removal in one pass -- runnable code deleted (warmFromPkmn.ts, warm:pkmn), migration 052_remove_pkmn_source.sql authored but NOT applied (re-labels the 9 real card_variant rows to source='tcgdex', verified against PGlite, not run against production), the upstream allow-list's comments and tests rewritten to explain the allow-list default instead of naming the excluded host, the research corpus sorted per-file (three competitor-reverse-engineering docs deleted, four DeckPal-original docs kept and reworded, CARD-ART-SOURCES.md's ruling kept intact deliberately), ~20 files of product-lineage comments reworded to describe behaviour not origin (including the pkmnDark root class renamed deckpalDark), and the decision log / skills updated to carry the policy without the name (DECISIONS.md's own past entries left untouched, append-only). Flagged, not silently skipped: wiki sync outstanding (this row and the linked Decision-Log/Data-Layer/UI-Spec updates close that item). |
| 2026-08-31 | Claude Fable 5 | @cheyras | Project Holo 2c, part 2: card-art re-sourcing executed. Owner approved pokemontcg.io (§7 of CARD-ART-SOURCES.md) as the card-art fallback and approved deleting objects with no approved source. Dump measured 1,912 affected image_asset rows / 914 cards; a TCGdex↔pokemontcg.io crosswalk (173/218 sets, all 120 swsh-TG cards recovered) drove re-sourcing of 1,417 assets through the shipped storage:backfill --prefix images --force choke point (0 failures, 46 undersized-flagged); the remaining 495 had no approved source and, with their objects, were owner-approved-deleted (493 via the shipped deleteObject, 2 via a direct storage call because Unown !'s localId is refused outright by the key sanitizer -- a finding, not a fix, logged for later). Verification battery all-zero (no unapproved sources, no orphan rows, 10/10 etag spot-check). Published research/card-art-unavailable.json (952 cards, live no-art list) and rederived the 2026-08-26 no-source residue under the new crosswalk (504 -> 485, mostly cel25cc). Outstanding, named not done: a storage:backfill --reconcile pass (60 objects lack their image_object row) and the open decision on adding images.pokemontcg.io to IMAGE_SOURCE_HOSTS. This session also did the paperwork: DECISIONS.md entry, CARD-ART-SOURCES.md §8, card-art-residue.json rederivation annotation, .gitignore coverage for the pipeline's scratch directories, and this wiki sync (Decision-Log, Data-Layer, UI-Spec, Contribution-Record). |
| 2026-09-05 | Claude Opus 5 | @cheyras | Pay-what-you-want billing, end to end, on branch feat/pwyw-billing (worktree, committed, not pushed). Migrations 053 (tables) + 054 (@supabase-only RLS, table privileges, three SECURITY DEFINER write functions); apps/api/src/billing/ (Stripe client + gate, service, store + promptDue, webhook) and routes/billing.ts; apps/web/src/components/billing/ (amount chooser, Payment Element form, trust markers, the shared flow, the prompt modal, the profile card) plus a /design gallery. The load-bearing decision: billing_account is SELECT-only to its own subject, because stripe_customer_id is a POINTER INTO STRIPE and a repointable row would read back a stranger's card summary and bill their card — RLS plus REVOKE ALL/GRANT SELECT (Supabase grants writes on new public tables by default) plus SECURITY DEFINER functions that take the account from auth.uid(), never from an argument. 053 backfills existing accounts at the visit threshold with onboarded_at stamped, so they are asked on their next sign-in and get the check-in copy rather than a welcome flow. Verified: both migrations against real Postgres (PGlite) with Supabase's own blanket GRANT applied first so the REVOKE was genuinely under test — 34 checks, all passing; 22 pure assertions on promptDue/normalizeAmountCents wired into CI; full typecheck, both builds, check-functions 4/4; every UI state looked at at 1440px and 390px. Named not done: UNVERIFIED against real Stripe — no key exists in the build environment and per B9 an agent does not create one or set Vercel variables; the runbook is DEPLOYMENT.md → "Turning the pay-what-you-want tier on". Paperwork: DECISIONS.md, DEPLOYMENT.md (4 env rows + runbook), API.md, SECURITY.md (§ Payments), ARCHITECTURE.md, README.md, .env.example, and this wiki sync (Decision-Log, Architecture, Contribution-Record). |
| 2026-09-07 | Claude Opus 5 (1M context) | @cheyras | The DeckPal logomark replaces the app-icon-plus-typeset-name lockup, on branch feat/deckpal-logomark. Four owner-supplied SVGs (full wordmark and D monogram, each light-on-dark and dark-on-light) ship byte-for-byte in apps/web/public/logo/; <BrandLogo> and <BrandD> replace <BrandMark> across five sites -- expanded sidebar, collapsed sidebar (the D), mobile top bar, /auth, and the landing nav and footer. Served as <img> rather than inline SVG deliberately: each file carries its own <style> block on generic .cls-N selectors and linear-gradient ids, which would collide document-wide the moment the wordmark and the D shared a page -- which they do while the sidebar animates. Placed in public/logo/ rather than assets/brand/ (that holds sources a script renders, and vector has no render step, so a second copy could only drift) or public/brand/ (that directory's own README states it holds other people's trademarks). The app renders the white pair only, being dark-only; the dark pair ships for the README's light mode and is excluded from the service-worker precache. .brand-wordmark deleted from theme.css and premium.css -- its only job was typesetting the name beside the icon, and the name is drawn now. The link-preview card follows and its stated rationale inverts: gen-og-image.mjs rendered in a browser because the wordmark was not a picture, and it is one now, so the card draws the same file the app does (its static server needed .svg in its MIME map, or it screenshots a broken image); regenerated and eyeballed. README gains the mark via <picture>, so it follows GitHub's light and dark themes. Paperwork: DECISIONS.md, ICONS-NOTICE.md (rewritten to cover brand artwork generally, with a provenance table), DESIGN-SYSTEM-AUDIT.md, DESIGN-SYSTEM-PLAN.md, README.md, and this wiki sync. UI-Spec deliberately untouched: its header/sidebar lockup rows are measurements of pkmn.gg, not our chrome. Verified in a browser at 1440 and 390px, signed out and signed in as the QA account, every site plus no horizontal overflow at 390 with the full signed-in header; whole-workspace typecheck, web build, check-precache, check-api-base and check-auth-deadlines all clean. |
| 2026-09-08 | Claude Opus 5 | @cheyras | Pay-what-you-want billing went live on deckpal.app (PRs #152, #157, #160). Took the branch from committed-not-pushed to taking real money: created the live Stripe objects by CLI (support product, webhook endpoint with all 16 handled events including the newly-added payment_intent.succeeded, deckpal.app registered for wallets, a default portal configuration deliberately without cancellation because the app owns that flow), set the non-secret Production variables, ran the go-live cleanup once (1 stale customer, 13 experiment rows, 8 ledger rows cleared; all 10 accounts, onboarded_at and ab_presets kept), applied migrations 057-063 — seven, not the six every document claimed, because production was at 056 and migrate:status from the main checkout reports "0 pending" when it holds none of the files — and verified the schema against the catalog rather than the runner's output. Before the merge: a four-way parallel sweep (self-host branch, connection budget, webhook serverless edge, the runbook itself) that found the runbook would have produced a broken deployment three independent ways, and that the cleanup SQL was unwrapped and unguarded; and a rate limit on /me/billing after CodeQL flagged the mount. Then the first live payment failed, and the cause is the session's lesson: finishFirstPayment confirmed a subscription's first invoice with off_session: true, which Stripe rejects outright whenever setup_future_usage is set — which it always is on a first invoice — so the path could never have worked for anybody, and fifty rounds of review missed it because every one of them ran against a Stripe stand-in we wrote ourselves. Fixed on-session; a second bug of the same family (Link attaches a type: 'link' PaymentMethod with no card object, which ensureDefaultPaymentMethod refused, so a Link-only supporter could not subscribe at all) fixed alongside, with card in the API now describing any method and last4 nullable. The correction to the METHOD is scripts/stripe-contract-check.mjs — drives the Stripe CLI against test mode, asserts our calls are calls Stripe accepts, reproduces the go-live bug on demand so the guard can fail, and caught two false passes in itself while being written. Also fixed: three honesty defects that turned one failure into three (the log withheld Stripe's message, a rejected request was reported as "maybe charged", the profile invented a bank challenge). Named not done: the connection-per-request shape (observed live as EMAXCONN taking every route down under polling), the webhook sharing the request pool, ourSubscriptions listed three times per write, and removing the ?prompt= override. Paperwork: DECISIONS.md, DEPLOYMENT.md (go-live record + contract-check section), API.md, README.md, and this wiki sync. |
| 2026-09-12 | GPT-6 Astra | @cheyras | Documentation worker (Ringer). Recorded the security-ingress and UI-chrome contracts plus repo-hygiene facts into the canonical docs and wiki, on ui-final and security-fixes. Documented final intended behaviour only — no CI pass, PR merge or production measurement claimed. SECURITY.md, DEPLOYMENT.md, API.md gained the exact rate limits (REST pre-auth 600/min per source IP per process; per-user /tokens 20/min, /avatar 10/min, /oauth 30/min after auth but before the RLS pool.connect; self-host images health 60/min and a shared 3000/min asset budget), Retry-After (seconds), platform-aware identity (Vercel x-vercel-forwarded-for/x-forwarded-for; socket peer off-Vercel; trust proxy stays false; no new env var), and the per-process-not-distributed-quota limitation. ARCHITECTURE.md gained the request-pipeline order. apps/mcp/SPEC.md and wiki MCP-Setup clarify the REST guard does NOT automatically cover the /mcp transport, while the token/OAuth management REST endpoints do. Also recorded sniffContentType (octet-stream for malformed input, genuine typed-array views kept), the closed-set migration-CLI error formatter (no regex redactor; getters avoided and Proxy-trap exceptions caught without leaking DSN/message/stack), and dependency advisories (sharp>=0.35.4, hono>=4.13.5, qs>=6.16.0, fast-uri>=3.1.6; resolved in pnpm-lock.yaml; github-script v7→v9 is an Actions major). UI contract logged concisely (not a UI-Spec rewrite): #153/#174 counters/badges above lifted art + hover-only-on-fine-pointer + keyboard focus kept; #173 mobile name+stepper then price row, desktop 3 columns; #154 one mobile Actions dropdown + controlled purchase dialog + text-set-name logo fallback; #162 desktop wordmark 30→22.5 / D 33→24.75, mobile 22 unchanged; #155 Overview/Trends tabs removed, chart/range/currency/30-day-delta/TopMovers/empty-states kept. Repo hygiene: 10 local + 2 remote verified-merged branches deleted, 12 stale refs pruned, 13 gone-upstream links cleared, bundles made before deletion, 15,059 WIP files rehashed unchanged; machine-specific backup paths kept out of portable docs. Mirrored canonical DECISIONS.md → wiki Decision-Log and ARCHITECTURE.md → wiki Architecture (byte-for-byte) with updated footers; UI-Spec untouched (pkmn.gg reference). Frontend-Research assessed not stale for this work. |
| 2026-09-12 | GLM-5.2 | @cheyras | Repo-level pnpm pin (Ringer mechanical worker, GLM lane). Added package.json "packageManager": "pnpm@10.34.5" and removed pnpm/action-setup@v6's with.version: 10 input so CI infers the version from the pin; no dependency version, lockfile or workspace config changed and no global pnpm reinstalled. Added DEPLOYMENT.md prerequisite and a DECISIONS.md entry, mirrored wiki Decision-Log.md, and added this row. Auto-selection confirmed on this machine's native pnpm11.13.1 launcher (manage-package-manager-versions default), not promised for all configurations; a non-global npx --yes pnpm@10.34.5 fallback is documented. |
| 2026-09-12 | GLM-5.2 | @cheyras | Three.js 0.186.0 visual-review decision (Ringer documentation worker, GLM lane). Root visual review by GPT-6 Astra approved Three 0.185.1→0.186.0 for integration after owner PR CI. Retained six PR #179 updates (@ai-sdk/gateway 4.0.75→4.0.76, ai 7.0.93→7.0.94, three 0.185.1→0.186.0, TanStack Router 1.170.32→1.170.33, Virtual 3.14.10→3.14.11, html2canvas-pro 2.4.1→2.4.2) and reversed only the unexplained @types/node 26.5.1→26.5.0 lock downgrade back to 26.5.1; pnpm@10.34.5 pin preserved. Visual evidence: 10 still cases + 7 motion frames, largest still MAE 0.00413 on 0–255 scale, max channel delta 2/255, two fresh baselines exact; Chromium SwiftShader only. Passed 27 CI-equivalent checks, 97 security tests, 22 browser assertions, zero audit findings. NOT claimed: PR merged, production deployed, GitHub CI passed. Appended DECISIONS.md, mirrored wiki Decision-Log, updated Frontend-Research F.1 (pin→0.186.0) and F.2 (stage.ts custom Blender AgX via CustomToneMapping with stock AgX fallback), and added this row. |
| 2026-09-12 | GLM-5.2 (Ringer worker) | @cheyras | Documentation sync on the integration checkout for three approved branch changes (verified integrated branch, ready for GitHub CI and merge; not yet merged, not yet deployed): (1) new shared read-only card_price_history tool in packages/agent-tools (24 tools now, 13 read / 11 write) delegating to GET /cards/:cardId/prices with the verbatim MAY/MAY NOT contract; (2) Upcoming Sets "Coming Soon" rows on series detail, outside DB/canonical ids; (3) B10 bug-report doc correction — isCloudMode = !!(GITHUB_TOKEN && GITHUB_REPO), not merely SUPABASE_MODE. Updated repo README/API/ARCHITECTURE/SECURITY/AGENTS/SPEC/DECISIONS/PLAN + cards.ts JSDoc only; wiki MCP-Setup (corrected tools live in shared packages/agent-tools + two adapters, not inside apps/mcp), Architecture, Decision-Log, Home, and this ledger. Verification: 38 check commands all pass, 3,270 tests across 19 suites all pass, 15 browser gates true under mocked local API at desktop 1280 + mobile 390; one command returns 1 deliberately (tool negative control with finally restoration); GitHub CI and production deployment have not yet run. Runtime code unchanged; historical benchmark counts preserved. |
| 2026-09-12 | Codex in-session worker | @cheyras | Documented the adversarial-review repairs: series-only nullable YYYY-MM-DD release dates at the SQL boundary, stable real/upcoming ordering and Denver calendar-day display, plus actual-route coverage and non-default card_price_history range/currency request assertions. API.md and ARCHITECTURE.md state the scoped contract; DECISIONS.md records R1/R2 and the query-test gap, with prior history preserved. Mirrored canonical Architecture and Decision-Log and updated this ledger. Final verification, fresh Astra review, GitHub CI, merge, and deployment remain pending; no tests or production access performed by this documentation worker. |
| 2026-09-12 | Codex in-session worker | @cheyras | Documented the price-history pagination repair required by fresh GPT-6 Astra review: the actual conversational adapter truncated a three-variant 30d response with no continuation. Current README, Architecture, MCP SPEC and wiki MCP-Setup now explain tool-side offset, complete-record pages bounded at 5500 characters, and model-visible next_offset until completion; the REST request and global adapter cap remain unchanged. Appended the decision and mirrored canonical Architecture/Decision-Log with earlier history preserved. Final verification, Astra re-review, repair publication, GitHub CI, merge and deployment remain pending; this documentation worker executed no tests and made no production requests. |
| 2026-09-12 | Codex in-session worker | @cheyras | Documented the upcoming-logo production repair after PR #182: the tracked WebP returned 404 because Vercel upload filtering lacked its exact exception. DEPLOYMENT.md and the brand README now require both .gitignore/.vercelignore exceptions, describe the build gate derived from actual logoAssetPath literals, and explain BASE_URL for cloud / and self-host /deckpal/. Appended the decision and mirrored canonical Decision-Log, preserving prior history. Recorded supervisor-passed upload allow/deny and removal controls, missing-logo gate control, both builds, and desktop/390px browser checks. Fresh Astra re-review, follow-up CI/merge and repaired live smoke remain pending; this worker edited docs only and executed no tests or production requests. |
| 2026-09-12 | Codex in-session worker | @cheyras | Documented permanent CI boundary coverage and selective weak-test cleanup. B7 permits only the runner-created private disposable PostgreSQL cluster while production-targeting test:collection remains manual and excluded. CONTRIBUTING now describes pure/build, DB and browser/deployment workflows and reproducible commands; Architecture and Deployment document isolation, local fixtures, artifacts and coverage limits. CI workflow comments updated without behavior changes. Appended the decision and mirrored canonical Architecture/Decision-Log, preserving history. Local acceptance passed: 3,275 existing CI-equivalent tests; two fresh PostgreSQL clusters, each running five real cases in UTC and Denver; 12 browser groups with eight screenshots on Playwright 1.63.0/Chromium revision 1243; and all asset negative controls. PR #184 merged at 1feb8ab after final Astra approval with five independent probes passing, plus passing GitHub checks: pure suites (3,275 tests), DB (10 cases), browser (12 groups), CodeQL and Vercel preview. This worker edited documentation only and ran no tests. |
| 2026-09-13 | Codex in-session worker | @cheyras | Ingested the archived Capacitor iOS search into Capacitor-iOS-Research: seven issue summaries with direct links, saved status/date metadata, scanner-platform context and explicit historical limits. Preserved all 41,670 original r2.json bytes in research-data/capacitor-ios-github-search-2026-09-01.json and linked the page from Home and Frontend-Research. Published in wiki commit 87ea129 with the identical raw Git blob verified before guarded cleanup removed 90 files: r2.json, the superseded scanner prototype.html, and 88 tools/frame-survey files. The remaining scanner research was retained for diagnostic inputs and unique experiment evidence, with representative hashes checked unchanged. Mirrored the canonical cleanup decision; source cleanup changes also retire the standalone foil plan and current Holo references while preserving product functionality and historical records. This is research preservation, not a new native-app decision or current App Store-policy audit. |
| 2026-09-13 | Codex | @cheyras | Documented the super-admin and AI credit feature: practical owner guide, all 15 permissions and UI dependencies, contributor tools, account/connector controls, defaults, estimated credit formula/markup versus margin, independent frozen pack pricing, Stripe readiness/events, refund/debt/hold operations, and schema-first 064–067 rollout. Updated README, API, Architecture, Deployment, Security, Contributing, schema and MCP documentation; appended the decision and mirrored canonical Architecture/Decision-Log plus the owner guide in the wiki. Updated Home, Data-Layer, Frontend-Research and MCP-Setup; preserved prior history and ledger rows. Recorded implementation-worker local evidence including 17 financial SQL suites and 27 browser groups, with supervisor desktop/390px visual review. Follow-up documents wallet recovery skipping locked reservations and retrying later, one shared admin budget of 120 requests/60s and a separate wallet budget of 180/60s per user/API instance, with database checkout quotas unchanged. The supervisor reports three real recovery regressions and an independent Astra probe passing. A further interrupted-upgrade permissions finding adds explicit new-object PUBLIC/existing anon/authenticated revocations in creation migrations 064/066 before their independent commits; later narrow grants remain unchanged. Documented the safe intermediate access boundary and appended/mirrored its decision. The backend check passed all 54 disposable integration cases, with 52 cloud privilege denials after 064 and 36 after 066, PUBLIC closed/creator retained, UUID self-host without cloud roles, final authorized flows intact and cluster cleanup confirmed. Production schema/bootstrap/payment verification remains unestablished; deployment awaits access to the existing authorized workflow, so deployed configuration and Stripe subscriptions remain unverified. Documented the genuine express-rate-limit 8.7.0 integration and early session-limiter/RLS order while retaining quotas and authorization; API types, 33 actual limiter tests, six admin tests and the declared repair check passed, then supervisor Ringer baseline passed 1/1. Current review/CI status is maintained in PR #188. This documentation worker ran no product tests or production requests. |
| 2026-09-14 | Codex | @cheyras | Documented the reusable DataTable/toolbar API, exact usage example, controlled paging/sort/state/disclosure, semantic desktop/mobile rows and supported server versus complete-list filtering. Covered all six consumers, gallery discovery and registered browser boundaries in canonical Architecture/Decision/browser README and the isolated wiki Architecture/Decision-Log/UI-Spec/Frontend-Research/Administration. Core tests/types and the complete 57-group isolated browser suite passed, with real table/gallery interactions and directly viewed desktop/390px screenshots, keyboard movement and compact row-action geometry. The branch preview is available; these local fixtures do not prove a production frontend release or live payment behavior. Refreshed exact source hashes and the wiki current-release record from verified migration/bootstrap/owner-access evidence; live Stripe remains unverified. Earlier decision and ledger history is preserved; the final documentation refresh changed docs only. |
| 2026-09-15 | Codex | @cheyras | Synchronized source and wiki documentation for canonical single roles, protected Owner, Contributor Dev tools and personal self-service, lifecycle/opt-ins, Owner-only unlimited/markup revisions, durable provider usage and current-epoch sharing, owned history correlation, observed-cost estimate drafting, exact webhook readiness and schema-first 068–071 migration/mapping/rollback guidance. Added the standing shared SVG Icon rule and corresponding table/UI guidance; appended the dated decision while preserving historical decisions. Verified exact source/doc/wiki excerpts and final Markdown hashes. The subsequent review correction documents current admin.access for usage, atomic operation/credit starts with known-uninvoked compensation, and mixed historical/current financial snapshots. Implementation evidence includes 502 local credit/Deck-E tests and the full disposable SQL runner with the real history HTTP chain in four UUID fixtures. This documentation pass made no live calls, commits, pushes or deployments; live rollout/payment/pooler state is separate operator evidence. |
| 2026-09-16 | GPT-6 | @cheyras | Completed the administration production rollout from PR #188, merged at 270665b0c41a74a0a07a3d320363d2d5ae6137a2 on September 15. All final technical GitHub gates passed, including 119 browser cases, disposable PostgreSQL and CodeQL; three independent Astra reviews and a supplemental review approved the application and final test-only limiter-wiring correction. Local evidence also includes 506 credit/SDK tests, 218 disposable PostgreSQL cases, 33 rate-limit cases and 881 remaining pure CI cases. Applied reviewed migrations 068–071 through the unchanged normal runner, with no pending migrations and Owner/schema/cleanup postflight verification. Production Stripe retained its 16 subscriptions and added all nine credit events; exact readback and a harmless signed diagnostic passed. On September 16, independently verified production deployment dpl_3FUj3eVsGEfrMdYMzuQZALWHiQPc serves the merged SHA at deckpal.app/admin, with database up, administration ready, Owner configured and live billing configured. Ten app-shell routes returned 200 and four protected APIs denied anonymous requests with 401; updated and verified the default Vercel alias to the same release. Seven annotation threads are resolved. The preview-only billing thread remains open: automatic approval review rejected broadening test credentials to all preview branches, and the owner's scope choice is pending. No real payment, Stripe-originated delivery or authenticated live browser session is claimed as verified. Published the eight reviewed wiki pages and preserved the scanner redesign WIP and saved stash. |
| 2026-09-19 | Codex / Ringer | @cheyras | On behalf of @cheyras, completed the 30th Celebration release record. PR #189 restored all four scheduled price/catalog workflows by retaining package.json as the sole pnpm-version authority; the earlier EUR recovery run passed. PR #191 merged at a981c539944458eeb3f9fb23b77b60410e87d561, adding bounded source-verified mappings for English 30th/24722 and 30th-c/24837, calendar-date and bundled-logo fixes, and sole-authority snapshot tracking. Final catalog, forced USD-price, and snapshot workflows (35484723147, 35484937386, 35485038789) succeeded at that SHA. Production APIs returned 158 main plus 30 Classic cards, and all 188 mapped identities have non-null USD market prices. The same-day snapshot correctly completed ok with zero inserted rows; lock/active-run skips record no completion, while failures mark failed and rethrow. Live 1440/390 browser observations verified Sep 16, 2026, the 448 px logo fallback, visible prices, and decoded main art. All 316 main low/high WebPs retain byte and provenance evidence. The 60 Classic paths remain missing from both approved upstream sources; no art or source was invented. The unrelated 10,532-object global drift remains untouched, and cloud phash scanner support is not claimed. Mirrored canonical decisions, corrected the release data-layer record, and preserved all earlier ledger history. |
| 2026-09-21 | art-doc worker (Claude Sonnet 4.6) | @cheyras | Documented 30th Celebration Classic Collection card art sourcing: all 30 me55c images located at images.scrydex.com (official CDN successor to pokemontcg.io), PNG 654×914, verified via pokemontcg.io API. Adoption pending Scrydex written permission — finding images at a migrated CDN domain does not authorize rehosting. Published research/30TH-CLASSIC-ARTWORK.md and research/30th-classic-image-crosswalk.json (30-card DeckPal-ID → pokemontcg.io-ID crosswalk incl. suffix disambiguation for duplicate collector numbers). Set-fill operator confirmed three TCGdex set objects via assets.tcgdex.net: sets/30th/logo.webp (735×401), sets/30th/symbol.webp (40×40 parent 30th symbol), sets/30th-c/logo.webp (735×401). Zero scoped drift; source URLs persisted. No Classic-specific symbol verified. No Scrydex card images published. Fixed stale brand README (both sets released Sep 16; logo now permanent bundled fallback via BUNDLED_SET_LOGOS for 30th + 30th-c). Added one sourcing-skill lesson bullet to CARD-ART-SOURCES.md. Appended 2026-09-21 DECISIONS.md entry. Mirrored canonical Decision-Log, updated Data-Layer 30th-art paragraph, appended this Contribution-Record row. No allowlist changes. PR #193 merged at 59f7a696edcdec748178cd26b9c90720f4984fcb after every exact-head CI check passed. Vercel production deployment succeeded. Read-only live browser checks verified both 735x401 logos, the 40x40 parent symbol, Classic text-symbol fallback, and series placement at 1440px and 390px; screenshots were visually reviewed. Scrydex card scans remain unpublished pending permission. |
| 2026-09-22 | Codex / Ringer | @cheyras | Ingested 29 Todoist tasks; flagged physical iPhone/data/migration-input blockers. Verified/completed existing GLB optimization (PR97 exact live 606796 bytes); verified Preview/Production DeckE key presence and production signed/ready health; disabled only require_code_owner_review in GitHub ruleset 20607708 while retaining required approval and CI; repaired mixed browser/approval replay in PR194. Evidence: local 685/685 DeckE tests, 7/7 independent real SDK cases, typecheck, and visual inspection at Chromium 1280/390. PR194 merged at e18daa69176d6da00b80f65199d0d8b086cad874 after all exact-head CI gates passed, including fullbrowser. Production deployment dpl_CFWmyGhmqaYBwT56uUMwXNVGNMz9 is ready; deckpal.app aliases to it, and GitHub production deployment 6597394671 matches the merge SHA. Live shell/JS returned 200; health was ok, database up, and signed approvals verified. No live model call or payment was performed. Evidence: /mnt/c/Users/cheyr/Documents/Codex/deckpal-todoist-20260922/chat-live-receipt.json (UTF-8 BOM). |
| 2026-09-22 | Codex / Ringer | @cheyras | With Claude Opus 5 implementation and Claude Sonnet 5 docs, repaired repeated terminal meter refusals across server steps and browser approval requests in PR #195, final head f8d4fc46158549027e4c851d6b18bfe6db85f6ee. Daily caps and holds suppress the whole deep tier; insufficient-credits refusals suppress only the matching call; a new user turn resets the ledger. Validation: 509 API and 698 web tests, clean typechecks, an actual Chromium 1280/390 run capturing the outgoing wire, independent real-SDK proof showing zero repeated deep charges and one independent approved write, plus negative controls -- all green at the exact head, including the full browser run. PR #195 merged as 371c563447755a8860cdb126857413cbf479e441 and deployed live to production (deployment 6599004860, Vercel dpl_5BZdMbPLkdp8WarRV3L9q3Z16JW3), which owns deckpal.app. New asset /assets/index-D15FLI1Z.js serves with page status 200; health is ok, db up, approvals signed, and 11/11 models ok. No live paid-model or payment test was run. Todoist task 6hJmQFx47PJFc6Cw is now completed by the driver. |
| 2026-09-22 | GPT-5.6 Sol | @cheyras | PR196 merged at final head 856e89795c99d619d9ea4652febd8b498638b8ba (merge SHA 669e34f3e0657d1a09d75450bcb879ead795a106); live deployment 6601242178, deploymentId 6601242178, Vercel dpl_3Hj4r6F8PRfqvbF8UPJDvqWembJ3, deployment https://deckpal-lvtmwi8o0-deck-pal.vercel.app, live alias https://deckpal.app, 200 page/asset, healthy DB and signed approvals. Verified scanner suite, web typecheck, full web build guards, real Chromium OffscreenCanvas/DOM at 1280/390, raw pre-JPEG RGBA capture equality, independent buffers, JPEG MIME/dimensions/tolerance, and visual inspection. No paid model or payment test. Todoist6hJWCpj6FhjPQGFP remains partial: no validated foil classifier consumes the raw buffer. |
| 2026-09-22 | Codex / Ringer | @cheyras | Completed task 6hRJPXG5RwwppMpP: PR #197 merged at 592fbc5ed6f853cbd7f26c66d0fcf71f4fa50c3c and deployed to production as 6602729938 at deckpal.app. Current-account payment history for support and credits, including one-time contributions, returned 200; anonymous access returned 401 and invalid cursors 400. Local 8 API + 4 formatting tests, 14 independent adversarial assertions, and real-component desktop 1280/mobile 390 proof passed; full GitHub CI, browser, database, and security gates passed. Production QA reads and anonymous/cursor rejection passed. No payments were made. Coverage excludes older replaced/deleted billing accounts; this does not claim a permanent complete payment ledger. |
| 2026-09-22 | Codex research worker | @cheyras | Task 6hJWPgW22rrhCx9P: published source-backed, conditional Vercel AI Gateway rip-session voice/transcription research. Research only: no application/infrastructure change, no paid provider call, and no physical iPhone validation. |
| 2026-09-22 | Codex / Ringer | @cheyras | Completed source-discovery documentation for Todoist task 6hJWCm3QjVGWCJxw: recorded one manually inspected English MEW 151 Caterpie 010/165 normal catalog reference and eBay reverse-holo photo (n=1), with receipt, URLs, hashes and limitations. This is a manual reference pair only: no pixels published, no rights inferred, and classifier/capture/community pipeline work remains open. |
| 2026-09-22 | Codex / Ringer research worker | @cheyras | Partial research session for task 6hJWCj2xqJ7X9vxw: documented all 11 eras with two exploratory manual comparisons total (SV normal/reverse and EX Dark Crobat holo/reverse), six inspected public images, and unresolved e-Card/HGSS candidates. Recorded source URLs, metadata bindings, observation limits, and next controlled-capture requirements. No pixels published, no classifier claim, no task completion claim. |
| 2026-09-22 | Codex / Ringer | @cheyras | Deck-E opening-write benchmark preparation under approved USD2; offline signed-approval/cost/readiness checks passed; live read-only QA preflight found Deck-E permission disabled with 1835 credits and no hold; zero model requests/USD0 spent; empirical stall rate remains unmeasured/task open awaiting QA access. |
| 2026-09-23 | GPT-5.6 Luna / Ringer | @cheyras | Verified the Deck-E fix for Todoist task 6hJpP8GwfxCm7MmP. The original short owned-card baseline was 7/10 signed approvals. QA Deck-E access was enabled under the explicitly authorized USD2 Gateway budget. PR198 (https://github.com/cheyras/deckpal/pull/198) reached 26/26 signed apply openings, followed by two preview-only probes exposing quantity-semantics errors, including one diagnostic that captured quantity:1 instead of delta:1; it split chat apply-intent log_cards from read-only preview_card_changes, preserving shared/MCP preview defaults and signed-approval security. PR199 (https://github.com/cheyras/deckpal/pull/199) reached 9/10 signed owned-card openings and 2/2 correct previews, leaving one prose-confirmation stall, and clarified relative delta versus explicit total quantity. PR200 (https://github.com/cheyras/deckpal/pull/200) final acceptance remains 30/30 signed apply openings and 4/4 read-only previews with correct relative quantities, and made call timing explicit: calling requests approval and execution remains held. All exact-head CI gates passed before each merge. Final merge SHA 264d8a31edf20c179abdda69475d664025245982; production deployment ID 6617225070; 95 paid QA turns; actual Gateway cost $1.16896030, below USD2. Zero collection writes occurred; approvals were held and never replayed. This was a finite sample, not a universal no-stall guarantee; actual model identity was unavailable from Gateway history. Prior local SDK/security checks covered current and legacy signed replay, tamper, decline and preflight cases. Production write replay was not tested, and the other 17 needs-input tasks were not completed. |
| 2026-09-26 | Claude Sonnet 5 | @cheyras | PERF-03: virtualized ListDetail's Table view (PR #217, not yet merged). Measured 3,200-card scale fixture: 29,077 → 485 DOM nodes and 3,041-3,538 ms → 88-120 ms render time at both 390×844 and 1440×900, before/after back to back under the same machine load. Confirmed sort and keyboard Tab+Enter still work correctly under virtualization; confirmed "Print checklist" is unaffected (server-rendered PDF, not client DOM). Checked DataTable.tsx as a shared fix point and confirmed it doesn't apply. Root-caused (not guessed) why the new column header can't be position: sticky: theme.css's overflow-x: hidden on html, body computes overflow-y: auto on both per the CSS Overflow spec, so <body> shadows <html> as the sticky scroll container — likely also silently breaks DeckBuilder's and CardDetail's existing sticky elements; flagged as a separate follow-up task, not fixed here. Added a standalone browser regression test (pnpm test:browser:list-table). Full pnpm test:browser was not run this session (concurrent machine load); targeted typecheck, build, unit tests and the new browser test all passed; CI is the pending gate. |
| 2026-09-28 | Claude Opus 5.5 (directing Codex GPT-5.6 workers) | @cheyras | Deck-E chat overhaul (branch decke-chat-overhaul): diagnosed the owner's Deck-E conversations, moved the chat to Claude Sonnet 5.5, full tool-result replay and conversation-long exact declines, retired the deep sub-agents, web_research without approval and with sources, check_deck + showDeck, one live activity line, animation mapped to activity, rewritten prompt, replay probe. Decision: 2026-09-28 Deck-E thinks with Claude. |
| 2026-10-01 | Claude Sonnet 5.5 (reviewed by two fresh-context Opus 5.5 rounds) | @cheyras | Priced the cards TCGdex has no TCGplayer id for (PR #272, merged as a5b16a16): svp-085 Pikachu with Grey Felt Hat showed no price although TCGplayer lists it at $1,031.27, and the whole SVP set and Jungle were unpriced. A TCGCSV link pass derives the group and product ids on number AND name and refuses every ambiguity. Review round one found two wrong-price paths on real data (a patterned sibling's id copied onto a plain variant; a name-only fallback accepting stamped products) and round two found a stuck-running row that would have stopped all price ingestion; all fixed with tests, plus a real-Postgres suite. Card art deliberately not touched (sourcing rulings in research/CARD-ART-SOURCES.md). Decision: 2026-10-01 TCGplayer ids TCGdex lacks are now derived from TCGCSV. |
| 2026-10-04 | Claude Opus 5.5 (reviewed by fresh-context Opus 5.5 rounds) | @cheyras | Rebuilt the logged-out landing from the owner's page map and copy (PR #275). The photos are generated scenes with real card art composited onto blank faces. The page shows the AI apps' real logos with a no-affiliation line, and a new public /connect help page covers connecting. All copy is in one module that also feeds the crawler prerender, llms.txt and the JSON-LD. Review fixes: robots.txt no longer blocks /api/ (Google would have indexed the catalog as empty), WebApplication JSON-LD is on every shell, pausing a chat stops all its motion, /connect is signed-in aware, and the security-headers crawl excuses the loopback Storage stand-in. Follow-up PR #276 keeps the hero phone's pause button off its Example tag. Verified live on deckpal.app at 1440 and 390: no broken images, CSP violations or console errors. Open: dist/landing.html is built but not served for /. A vercel.json rewrite never fires because Vercel serves index.html first; Routing Middleware or a shell rename needs the owner (B9). Also published the 2026-09-28 line below and synced Architecture from the repo's ARCHITECTURE.md, because that session's edits were never pushed. Decision: 2026-10-03 landing rebuilt. |
| 2026-10-04 | Claude Opus 5.5 (reviewed by fresh-context Opus 5.5 rounds) | @cheyras | Full SEO (PR #277, approved by the owner). Vercel Routing Middleware (middleware.ts) serves the prerendered landing for / and a new prerendered /connect, so crawlers that skip JavaScript get each page's words, title, description and canonical. A vercel.json rewrite had been proven dead on a preview, because Vercel serves index.html first. lib/seo.ts usePageMeta gives every public catalog page its own title, description and record-built canonical, replacing one shared title on 20,000+ pages. A real 404, search results, the sign-in pages and unknown URLs are noindex; a passing error only drops the canonical. Catalog sitemaps come from the live catalog at /sitemap-{pages,sets,cards,pokedex}.xml (25, 201, 21,291 and 1,025 URLs), CDN-cached and checked against real migrations by the Pocket-exclusion suite. The preview caught a sitemap 301 loop, caused by a named rewrite param arriving as ?file=. Verified live: / and /connect serve their prerendered documents with no noindex, each sitemap answers 200 and is a CDN HIT, deckscout.io still redirects, and real-browser titles and canonicals are correct on 13 routes. ARCHITECTURE section 13 is documented (PR #278). Open for the owner: Google Search Console verification (a TXT record on Vercel DNS) and sitemap submission. Decision: 2026-10-04 serve prerendered landing and connect pages through Routing Middleware. |
| 2026-10-04 | Claude Opus 5.5 (reviewed by a fresh-context Opus 5.5 agent) | @cheyras | deckpal.app/ sends signed-out visitors to the marketing landing, not sign-in (PR #279, merged as b4b17f90). The index route had sent anyone who had ever signed in on that browser to /auth, using the deckpal.returning localStorage marker; the redirect and the marker (lib/returningVisitor.ts, its writer and its clearer) are removed. Signed-in, self-host and session-read timeout still go to /series. Verified with Playwright on the preview and on deckpal.app: a fresh context and one with deckpal.returning=1 pre-seeded both stay on / and show the landing (before the deploy, the seeded case landed on /auth). Decision: 2026-10-04 signed-out visitors at the root always get the marketing page (supersedes 2026-08-20 "A lapsed session is not a stranger"). |
| 2026-10-04 | Claude Opus 5.5 (reviewed by a fresh-context Opus 5.5 agent) | @cheyras | The quad labeler queue no longer times out (PR #280, merged as 45a9243a). Listing the pending queue had locked every photo in the private dev-captures bucket and hit Vercel's FUNCTION_INVOCATION_TIMEOUT (504), so nothing had been labelled since 2026-09-27; it now reads one snapshot. The cold listing still costs about 30 s per 1,000 photos, so an overall deadline and a shared in-flight listing are open follow-ups. Decision: 2026-10-04 the labeler queue listing reads one snapshot. |
| 2026-10-04 | Claude Opus 5.5 | @cheyras | Fixed the storage test #280 added that Linux CI cancelled (PR #282, merged as 63013adf): AbortSignal.timeout does not hold the Node event loop open, so the hanging-fetch timeout test ended early; a keep-alive timer holds it. Verified in WSL Linux before pushing. |
| 2026-10-04 | Claude Opus 5.5 (reviewed by fresh-context Opus 5.5 agents) | @cheyras | Card-corner detector training prep (PR #281, merged as 3a551ed4). Harvests the quad-label corpus as the QA account into ~/deckpal-data (163 labels then, outside git because they hold the owner's photos), freezes one session-level split pinned by manifest sha256, scores the shipping LC050 detector through the engine path against the 2026-09-02 gates (baseline on-card 65.3%, below the 80% gate), and adds a PyTorch fine-tune pipeline whose export keeps LC050's exact I/O contract (parity about 1e-5, preprocessing bit-exact against the TypeScript). Close-ups are measured by card side (>= 80% of the square), not area, which an upright card caps at about 72%. Experiments: a pilot fine-tune fixed interior locks on synthetic close-ups but not normal framings; leave-one-day-out cross-validation improved only the folds with 123+ training rows; perfect corners would add about 5 of 88 front identifications, so rotation, missed detections and reprint ties cost more. No model shipped. Decision: 2026-10-04 card-corner detector training prep. |
| 2026-10-09 | Claude Opus 5.5 (reviewed by fresh-context Opus 5.5 agents, three rounds) | @cheyras | Scanner identification: a real-crop benchmark and the fixes it found (PR #284, merged as 7b667050). scripts/scan-bench replays 278 real crops from six owner sessions (labels audited by eye, outside git) through the shipping path with no database: server dHash, device OCR run in Node, image vector, resolve ladder, client reducer. It scores AUTO-ID (confident and right) and WRONG. Fixes: cleanNameRead (CJK glyph, stage badge, HP tail); a name+denominator rung for dropped numerator digits; a decisive vector the printed name agrees with resolves after an unconfident key rung, guarded so a real card name, a name-agreeing number and the near-exact dHash still win; dHash alone may name a card only at distance 7 or less. On 244 cards with OCR on, auto-ID went from 63.9% to 68.9% and wrong from 1.9% to 0.6%. Decision: 2026-10-09 scanner identification benchmark. |
| 2026-10-09 | Claude Opus 5.5 (reviewed by a fresh-context Opus 5.5 agent) | @cheyras | Scan telemetry keeps the full-size crop and what OCR read (PR #285, merged as 071e2f5c). Capture events carry the device's own 480×670 JPEG as rectifiedJpeg, which is smaller than the 320 px PNG beside it, and identity records carry ocrRead. The benchmark could score OCR on only 85 of 256 crops because telemetry had kept 229×320 copies. |
| 2026-10-09 | Claude Opus 5.5 (reviewed by a fresh-context Opus 5.5 agent) | @cheyras | OCR reads the printed number on deckpal.app for the owner (PR #286, merged as 23ce6f83). The device OCR lane was off on production; it is now on for Access.isOwner and stays off for everyone else, because scanner.use follows the feature lifecycle and can widen without a deploy. On full-resolution crops the printed key is worth +9.4 points of auto-identification. |
| 2026-10-09 | Claude Opus 5.5 (reviewed by a fresh-context Opus 5.5 agent) | @cheyras | The scanner warms its image model when it opens (PR #289, merged as 1efd5d7b). GET /api/scan/warm loads the identity model and runs it once on a blank crop, behind the scanner gate. The scanner calls it on open, every 4 minutes while visible, and on returning to the tab, and stops once the server reports the matcher off. In field telemetry /scan/embed took about 1.5 s warm and 4-8 s on a session's first captures. |
| 2026-10-09 | Claude Opus 5.5 (reviewed by a fresh-context Opus 5.5 agent) | @cheyras | Orientation fallback for sideways and upside-down captures (PR #290, merged as c8b61e0d). When the upright vector answer is not decisive, /scan/embed turns the input tensor 1-3 quarter turns from one decode and takes a turn only if it is decisive, with no new turn after 3 s of the request. On the fine-tuned model that gave +3 decisive right answers of 256 with 0 wrong; the zero-shot model gained one right and one wrong, so it is gated to deckpal-card-b32-v1 and stays inert until #288 ships. |
| 2026-10-10 | Claude Opus 5.5 | @cheyras | Deck-E battle logging fixed (#291): every tool in view from step one, settle rule, paste backstop, whole-log extraction, dry-run preview |
| 2026-10-10 | Claude Opus 5.5 | @cheyras | Battle simulator: @deckpal/sim rules engine, 13 account decks scripted, search CPU, simulate_battles + deck_odds tools |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Deck-E harness v2 (PR #293, merged as 1b1f6da7). Claude Haiku 5.5 triages every message; code picks Quick (Haiku 5.5) or Standard (Sonnet 5.5) from per-job pathway floors; one route per turn via an unsigned, bounded echo; an ask_user card for what only the reader knows; core prompt 29.6k→15.8k chars plus cached pathway texts. Replay probe: 12/16 at $0.028 a scenario vs Sonnet-only 10/14 at $0.049. Three fresh-context reviews; fixes include no double write on a resumed approval and follow-up legs that keep an approved write's result. Live on production (a rules question routed quick/card_rules). |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Deck-E fixes found during harness v2: the deck widget had never rendered since #267 (PR #301, merged as 07ad91e8); research ran on a model the Gateway no longer lists, now perplexity/sonar (PR #302, merged as 5e5f1a0c); every approved write is at most once per signed tool call (PR #304, merged as af6057f0); set_progress goal numbered (PR #300, merged as ea0ca0ca). |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Deck-E harness v2 follow-ups: triage accuracy eval on 145 labelled messages, code rules for declines and venting, held-out tier accuracy 71%→97.5% (PR #312, merged as 46376d8c); progress lines between batches of lookups, A/B 4/7 vs 1/6 long turns with progress at equal cost (PR #313, merged as b824b0cd); pathway texts checked against the tools (PR #314, merged as f41791ab). |
| 2026-10-10 | Claude Opus 5.5 | @cheyras | Scanner auto-capture look re-arm + lock dwell 3 -> 2 (#292, merged): a different card put down in the same place now captures; video replays 18% -> 31% captured, 10% -> 19% auto-ID, 0 confident-wrong; owner-session check 23/39 refused new cards rescued; reviewed by Astra (Codex) |
| 2026-10-10 | Claude Opus 5.5 | @cheyras | CI runs every scanner test: the API scan suites are picked up by glob, not a hand-kept list (PR #299, merged as defc414e). Reviewed by Astra (Codex). |
| 2026-10-10 | Claude Opus 5.5 | @cheyras | Scanner reprint guard (PR #294, merged as 6740445d; reviewed by Astra (Codex), including the merge with #297, where it caught a pinned-card loss that was then fixed). A picture shared by several printings names the card but not the printing; only a printed key the OCR reads names the printing. Same-art families come from ORB, dHash, a foil pass, the known reprint sets and the Celebrations Classic Collection. The hash may not name a WotC-era card on its own. On the owner's photos, confidently wrong answers fell from 23/105 to 4/77, and on the held-out set from 14/67 to 4/57. |
| 2026-10-10 | Claude Opus 5.5 | @cheyras | Scanner OCR digit drop (PR #297, merged as 6ea3b1a4). Reviewed by Astra (Codex). A printed name that names another card now questions a confident printed number. |
| 2026-10-10 | Claude Opus 5.5 | @cheyras | Scan bench owner-photo tools (PR #298, merged; reviewed by Astra (Codex) over four rounds): queue photos -> card crops -> proposed identities, verified by eye and kept outside git. |
| 2026-10-10 | Claude Opus 5.5 | @cheyras | Scanner OCR reads the bottom-right collector number of pre-2017 cards (PR #303, merged as ae9aa496). Reviewed by Astra (Codex). |
| 2026-10-10 | Claude Opus 5.5 | @cheyras | Look re-arm carries a capture's four quarter turns, so a sideways capture still matches its card. The re-arm is measured on the owner's sessions (PR #306, merged). Reviewed by Astra (Codex). |
| 2026-10-10 | Claude Opus 5.5 | @cheyras | Scanner detector: an optional second look for low presence, off by default, and an aimed virtual phone for the video bench (PR #307, merged as 5bde6c96). Reviewed by Astra (Codex). |
| 2026-10-10 | Claude Opus 5.5 | @cheyras | Video bench scorer (PR #308, merged as 626cd532). Captures are scored only inside the labelled window, and a capture taken while two cards are shown together may name either one. The 13 "confident-wrong" on the new videos were all scoring artefacts. Over 21 videos, #292's policy captures 29% and auto-identifies 17%, with 0 confident-wrong. Reviewed by Astra (Codex). |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Reprint guard follow-up (PR #311, merged as 9a551c3d). Each Celebrations Classic card now twins ONE original, matched by set, number and name. That is 25 twins, down from 35, and seven different pictures leave the same-art families, so a printed key reading 4/130 decides on its own again. Reviewed by Astra (Codex). |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Scanner detector: a second presence head for busy backgrounds, max'd with the original (PR #317, merged as 2079d192). The original head is kept byte for byte, and a refit head fitted on 20 CC BY videos plus the owner's phone frames is added; has_obj = max of the two. Held out, through the shipping policy: captured 29% -> 47%, auto-ID 16% -> 23%, never below the original on any phone frame. Astra caught the first version (a replaced, video-only head) dropping tabletop phone scans; the fix adds a presence regression test, and the offline harness now loads this checkout's model. |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Deck-E's deck widget saves a revision as the next version of the reader's own deck (PR #305, merged as 6cc2905e); found and fixed with it: the chat re-armed its composer focus on every host render, pulling keyboard focus off the widget and approval cards (in PR #315, merged as ff4ccffd). |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Battle logs v2 (PR #315, merged as ff4ccffd; migration 084 applied to production first): in-person games with a debrief card, countable opponent archetypes, Deck-E's reviews kept apart from notes, battle_digest for every client, a Quick-tier consult of Sonnet 5.5, and the Battles tab showing all of it. |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Deep Think (PR #316, merged as 7ecc6feb; migration 083 applied to production first): Deck-E offers Claude Opus 5.5 with a server-computed credit estimate and runs it only on the reader's signed, turn-bound OK; 8× credit hold, balance floor at the estimate's high end. |
| 2026-10-11 | Claude Opus 5.5 (directing Claude Sonnet 5.5 lanes) | @cheyras | Battle simulator: every Standard card scripted (PR #318, merged as d9325157): all 1,679 Standard texts with effects scripted from their printed text, each official ruling that changes behaviour cited and pinned by a test (281 more texts have nothing to script); about 60 engine capabilities in seven engine batches; Tera, Ancient/Future and printed Weakness/Resistance (341 printings) from pokemontcg.io; replay fixes; speed back to about 1.1x main. Sonnet batches of 25 cards, each re-verified (tsc, full suite, text round-trip, mutation gate, fuzz, replay ratchet) before merge; deterministic codegen for templated cards. Two Astra rounds: four P1s fixed (CPU estimates on public information only, face-up Prizes, evolution clearing delayed effects, suppressed Ability types), then approved. Production deploy verified; a live 120-game run plays every card in four real lists. Decision: 2026-10-11 Battle simulator: every Standard card scripted. |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Scanner identity embedding v2 goes live (PR #288, merged as edf4bae9). The fine-tuned deckpal-card-b32-v2 replaces zero-shot CLIP. Production prerequisites were run with the owner's permission: migration 082 applied; 20,467 cards embedded under the v2 stamp, a superset of the 20,308 the CLIP stamp covered; and the int8 query model staged and sha-verified, after .env.prod's revoked service key failed and the current key was read via the Supabase CLI. Reviewed by Astra (Codex), whose findings fixed bench vector defaults and the v2 runbook. |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | v2 follow-ups (PR #322, merged as 25f78c1f): the API model README now names the int8 query export as distinct from the fp32 catalogue export, and gate_sweep.py counts card backs as negatives. Reviewed by Astra (Codex). |
| 2026-10-11 | Claude Opus 5.5 (directing Claude Sonnet 5.5 lanes) | @cheyras | Simulator new-set, rotation and rulings-update pipeline (PR #320, merged as e7a1e831): scripts that sync new frames, refresh the pokemontcg.io card tables, lane new cards for scripting and diff the official rulings, plus a Standard coverage gate in CI. Rulings are committed as an index only (id, date, categories, source, hash), never their text. Reviewed by Astra (Codex); a CodeQL tag-stripping finding was fixed. Decision: 2026-10-11 Simulator: new-set, rotation and rulings-update pipeline with a Standard coverage gate. |
| 2026-10-11 | Claude Opus 5.5 (directing Claude Sonnet 5.5 lanes) | @cheyras | Battle simulator built-in meta pool (PR #321, merged as 44ceed86): the Standard meta decks, refreshed from Limitless's public API by a script, as simulate opponents (meta, meta:<archetype>) and as the default when the reader has no other decks. Reviewed by Astra (Codex). Decision: 2026-10-11 Battle simulator: a built-in Standard meta pool, refreshed from Limitless, as simulate opponents. |
| 2026-10-11 | Claude Opus 5.5 (directing Claude Sonnet 5.5 lanes) | @cheyras | Battle simulator continuation (PR #324, merged as 44ef939f): a signed token lets Deck-E and MCP agents continue a run across calls for bigger samples, with no new infrastructure. The token is bound to the user and deployment, expires after 2 hours, allows at most 12 calls, and is redacted from diagnostics. Reviewed by Astra (Codex); a production QA run reached 96 cumulative games over 2 calls and a tampered token was refused. Decision: 2026-10-11 Battle simulator: continue a run across calls for bigger samples (signed continuation token). |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Battle simulator meta fallback (PR #327, merged as de2082d9): when none of the reader's other decks can be played yet, the default opponents fall back to the built-in meta pool instead of returning no games. Found by the production QA run on the QA account. Reviewed by Astra (Codex). |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Reprint guard: every basic energy opens its printing (PR #325, merged as f06e9805). v2 named basic energies of other eras confidently; their other printings (catalogue energy_type Normal) are now type mates, appended after the reader's list so same-picture printings are never crowded out. Owner photos: wrong 2 -> 1 with no right answer lost. Astra (Codex) caught the first version's 50-card families hiding choices. |
| 2026-10-11 | Claude Opus 5.5 (directing Claude Sonnet 5.5 lanes) | @cheyras | Battle simulator: the 30th Celebration set and Mega promos (PR #326, merged as ae8183ec): 120 texts scripted (28 from templates, 88 by hand, 4 needing new engine capabilities: a Weakness multiplier, Trainer interception, a heal lock and borrowed attacks), so every Standard printing in the catalog is covered (3,234 printings, 0 left). Catalog gaps are patched where frames enter the engine: HTML Energy spans cleaned to symbols, 33 missing evolution parents filled in, Trainers without text scripted from their reprints. Astra (Codex) round 1 raised four P1s, all fixed, then approved; two CodeQL alerts fixed. Production deploy verified and the QA run passed. Decision: 2026-10-11 Simulator: the 30th Celebration set, with catalog text gaps patched where frames enter the engine. |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Deck-E answers rules questions on Sonnet 5.5 (PR #319, merged as 79225a6a): Haiku 5.5 got a basic first-turn rule wrong 16 of 18 times across five setups, Sonnet 6 of 6. |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Jev routes Deck-E when it is sure (PR #323, merged as e5a49a8f): Jev and Haiku triage start together, and Jev's route is used at ≥0.9 confidence. On 148 eval cases the tier accuracy was within a point of Haiku's (held-out equal), Jev decided 62%, and routing p50 went from 1,181 ms to 294 ms. Production showed 262 ms against Haiku's 2,309 ms. DECKE_JEV_ROUTE=off is the rollback. |
| 2026-10-11 | Claude Opus 5.5 (directing Claude Sonnet 5.5 lanes) | @cheyras | Battle simulator rulings audit (PR #328, merged as fd6975bf): the 278 official Compendium rulings filed under a scripted card that no test pinned. 225 are now pinned by scenario tests, 43 are reviewed as not testable (each with a specific reason), and 10 are left open for engine work. Six scripts were fixed: Pyroar, Mystery Garden, Noctowl, Ninetales, N's Zoroark ex and Munkidori. Ruling text is never copied into the public repo. Twelve Sonnet batches were each re-verified. Astra (Codex) round 1 found a review entry hiding an engine bug (#2032), which was fixed, then approved. Decision: 2026-10-11 Battle simulator: every official ruling filed under a scripted card is pinned by a test or reviewed with a reason. |
| 2026-10-11 | Claude Opus 5.5 (directing a Claude Sonnet 5.5 engine lane) | @cheyras | Battle simulator engine follows 9 more official rulings (PR #329, merged as 0f2fbe0f), each pinned by a test: fixed attack costs beat cost increases; "N less damage" reductions apply to every target; Weakness and Resistance apply to an attack's damage to its own Active (confirmed against the Advanced Player's Rulebook v3.4, B-09); Resolute Heart settles at the Knock Out check; attacks that evolve work on the player's first turn; no Rare Candy after a devolve; no search with a full Bench; a Pokemon Knocked Out by evolving gets no evolve trigger; and Cinderace's setup choice. Reviewed by Astra (Codex), which approved with two P2 follow-ups. Decision: 2026-10-11 Battle simulator: the engine follows nine more official rulings. |
| 2026-10-11 | Claude Opus 5.5 | @cheyras | Fixed the flaky deck-import browser test (PR #330, merged as 88feccd8). The ten-edit burst is typed by the page's own timers instead of Node keystroke round-trips, so a CI stall can no longer open a gap longer than the 300 ms debounce ("got 3 checks"). Assertions are unchanged; shard 2 passed 3 of 3 runs locally. Reviewed by Astra (Codex). |
| 2026-10-11 | Claude Opus 5.5 (directing a Claude Sonnet 5.5 lane) | @cheyras | Battle simulator rulings follow-ups (PR #333, merged as 996dc600). Counters moved onto a protected Pokemon are lost (#2150: Team Rocket's Wobbuffet, Ninetales, Flutter Mane). Two Resolute Heart engine bugs are fixed: a second hit, and healing before the Knock Out check. The search CPU no longer loops on unlimited Abilities: it keeps the shortest retained win, which fixes the stalled fuzz game. Replay handles Cinderace's setup choice and cumulative mulligan lines. Coverage is added for #2122, #362, #588, #620 and rulebook B-09. Two Astra (Codex) rounds approved it, with all follow-ups fixed. Decision: 2026-10-11 Battle simulator: rulings follow-ups. |
| 2026-10-11 | Claude Opus 5.5 (directing a Claude Sonnet 5.5 engine lane) | @cheyras | Battle simulator: the right player orders effects that happen at the same time (PR #332, merged as 38934e93), following the Advanced Player's Rulebook v3.4. Damage triggers are ordered by the damaged Pokemon's player. KO step 2, Energy attach, evolve and end of turn are ordered by the turn player. Checkup is ordered by the next player, with the Special Condition checks as an orderable item. Each moment gets its own event id. Nested effects resolve right after their cause. Effects bound to different Pokemon are always asked. Rulings #146 and #2146 are pinned with both orders, so every strong-match ruling on a scripted card is now cited. Three Astra (Codex) rounds approved it, with all findings fixed. Decision: 2026-10-11 Battle simulator: the right player orders effects that happen at the same time. |
| 2026-10-11 | Claude Opus 5.5 (directing Claude Haiku 4.5 and Claude Sonnet 5.5 lanes) | @cheyras | Battle simulator: ruling answers quoted word for word in about 450 comments, notes and test names (115 files) are now short summaries in our own words, keeping every citation (PR #334, merged as 7c2cea42). A checker proved no code changed beyond 4 deliberate strings. Citation counts are unchanged, and no 12-word run of ruling text remains outside printed card text. Haiku lanes wrote the first pass. Astra (Codex) then found 22 of 123 sampled summaries unfaithful, so Sonnet lanes re-checked all 448 against their rulings (about 230 rewritten). A second Astra round found 7 of 118 still misleading; those were fixed before merge. |
| 2026-10-11 | Claude Opus 5.5 (directing a Claude Sonnet 5.5 lane) | @cheyras | Battle simulator CPU measured against real games and made stronger (PR #331, merged as 9821ecdb). New tools: pilot-agreement replays the 10 live logs and asks each pilot at every logged decision; pilot-h2h is a paired 9-deck head-to-head. The search pilot reuses finished positions within a turn, keyed by exactly what the player knows and never hidden cards. It values draw/search engines and scores wipeouts and deck-out risk better. A rare lethal-line crash and a Rocket Brain loop are fixed. Result: 51.7% [50.2, 53.2] over 4,320 games against the previous pilot at equal speed, with deck-out losses down from 240 to 87. Two Astra (Codex) rounds approved it, all findings fixed; the first measured 55% was reported down honestly as the memo was corrected. Decision: 2026-10-11 Simulator CPU: measured against real games and made stronger. |
| 2026-10-12 | Claude Opus 5.5 (with Claude Opus 5.5 research and docs subagents) | @cheyras | MCP tool surface audit (PR #335, merged as 61302e06; two Astra (Codex) rounds, round-1 findings and one CodeQL alert fixed, live on deckpal.app). pnpm mcp:audit measures tools/list (65,911 → 66,634 chars; $schema stripped, spec-accurate annotations added) and every read's result on a seeded disposable database, with budgets in the DB CI job. Unbounded reads now page: battle_logs include_raw at page_size 200 went from 949,283 to 23,060 chars, list items, the deck index, one operation's changes and revert plans are capped. Opponent and card names from pasted logs are quoted; revert rejects unknown arguments (a documented source was silently widening undos). TOOLS.md and the MCP README's tool list are generated from the registry with a drift test. Options left to the owner (merging write tools measured at 4–6% for 7 breaking renames; write-safety gaps; demo/read-only access) are in two decision files. |
| 2026-10-14 | Claude Opus 5.5 (finishing an idle session's PRs, with a Claude Sonnet 5.5 docs lane) | @cheyras | Picked up three PRs an idle session left open: docs, landing page and link preview corrected to say only true things (PR #337, merged as d34c04e2); read-only personal access tokens, with a revoke that retries and names the token when it cannot (PR #338, merged as 5f7c8eea; migration 085 applied to production and verified before merge); OAuth /token rate limits keyed on the verified grant instead of the IP or client_id (PR #339, merged as b14ab4d8; five Astra (Codex) rounds: per-IP lockout, refresh-reuse suppression, revoked-connection store fill and used-token bypass all fixed before GOOD TO GO). |
| 2026-10-14 | Claude Opus 5.5 (with two Claude Sonnet 5.5 audit lanes) | @cheyras | Phase 1 of the owner-approved 7-phase plan (truth layer) merged. PR #341, merged as 88270307: battle logs know which player is you — a PTCG Live name in the profile, learned after three games, never overwritten (migration 086 applied to production and verified; four Astra round-1 findings fixed, including a real-Postgres concurrency regression). PR #340, merged as 76bc2614: Tera/Ancient/Future tags in get_card and search_cards, and rules_lookup (core rules plus every Compendium ruling, DeckPal paraphrases with links). Eleven Astra rounds: a simulator fix (an evolved Pokémon may use a same-named Ability again), a ruling-index generator fix, all 433 ruling summaries audited against the official rulings (61 rewritten), browser CI building the simulator. Phases 2 and 3 next, then a pause. |
| 2026-10-14 | Claude Opus 5.5 | @cheyras | Phase 2, first PR (PR #342, merged as 862fa44a): battle_digest reads each game through the battle simulator's replay. It adds the board as each turn began (flagged approximate after any engine/log difference), every attack with printed damage, Live's breakdown and the HP its targets had (copied and spread attacks included), moments worth a look (self-Knock-Out, an attack that did no damage, near miss, timer turn, ending), a line of card data per card shown, and a review-worth score with reasons (the owner's "define a battle worth analysing"); detail: 'turns' adds every turn's board. Six Astra rounds: a polynomial-ReDoS fix (every log line bounded before any parser pattern, now that the API reaches the simulator's parser), boards only from the replay run the engine keeps, the final board after the last action, one line per printing, codeless cards described by the moves they were seen using. No migration. |
| 2026-10-14 | Claude Opus 5.5 | @cheyras | Phase 2, second PR (PR #343, merged as 91e59a0f): battle_turn_review (and GET /decks/:id/logs/:logId/turns/:turn/review, 20 a minute per user) asks the battle simulator's strongest pilot what it would have done on one of the reader's turns of a stored game. The replay stops at the reader's first main decision; a wide search over determinised worlds shortlists first actions and a focused search per shortlisted action ranks them, the reader's own always among them, each with its line, Prizes, Knock Outs and how far behind the best it is. Seven Astra rounds hardened the deck the search plays from: rebuilt from the deck version the game was logged against, copies counted per name and placed per game text (split reprints, Basic Energy, two Riolu printings), the Stadium in play counted, unseen listed cards built and older reprints played as the scripted card; Knock Outs from the engine's events, not "left play"; a guessed logged choice never marked as the reader's. Tool count 30 (19 read). No migration. |
| 2026-10-14 | Claude Opus 5.5 | @cheyras | Phase 3 (PR #344, merged as 9a2206ed): DeckPal hosts its agent playbooks, one source in packages/agent-tools/src/playbooks/texts.ts (an index and eleven playbooks, v2 of the skill the owner tested on 2026-10-13). Read by a read-only playbook tool (MCP and Deck-E; tools/list 31, 20 read), the public GET /playbooks (JSON, and ?format=markdown for a plain fetch) rendered at deckpal.app/skill, and a downloadable pointer skill (/skill/deckpal-skill.zip) that only points at them. The MCP server instructions and the four MCP prompts now state the playbooks' policy and defer to them. Four Astra rounds plus a full audit of every playbook claim against the tool code: arguments checked against the tool they are written beside (prose and tables), reviews read the battle's deck version and the player's notes, format passed to validation, endings split (Prizes, concessions, deck-outs, other), battle-log edits documented as not undoable, a misfiled log copied field for field (full played_at now shown) and checked before deletion, duplicates confirmed on the raw text. No migration. Phase 3 is the owner's stop point. |
| 2026-10-14 | Claude Opus 5.5 | @cheyras | Phase 4, first PR (PR #345, merged as 15a0176e): bulk list jobs in one call. search_cards rows show each card's finishes, and a finish filter keeps cards printed in one of them. edit_list add_search adds every card the same filters match (one shared cardFilters), every page, up to 500, choosing each card's printing by the finish order given, in the single bulk write. Three Astra rounds: an item removed and chosen again as the same printing is kept (a dynamic list's dedupe would otherwise delete it); text/damage refused rather than dropped; one 500 limit over add_cards, add_search and add_missing, checked before any write; a failed add leaves the removals undone; the search's counts in every reply. New DB integration suite bulkLists.mjs (14 cases, half on the real list routes). No migration. |
| 2026-10-14 | Claude Opus 5.5 | @cheyras | Phase 4, second PR (PR #346, merged as 590c73fd): paging the same way in every tool, page + page_size with a footer naming the next call and its page size (collection_log, mutation_history, decks and card_price_history brought in line; price-history pages packed deterministically, an oversized record its own error page); over MCP an argument a tool does not take is reported on the result's first line instead of silently stripped (validated loose, advertised unchanged; strict schemas still refuse); undo hints and creation ids end with "(for the tool, never the reply: …)". Three Astra rounds: card_price_history page_size, records after an oversized one reachable, page_size named in every continuation. Phase 4 complete. No migration. |
| 2026-10-14 | Claude Opus 5.5 | @cheyras | Phase 5, harness (PR #347, merged as ff08ce25): an eval of game reviews scored per PRINCIPLE (facts from a source, every surprise has a mechanism, consequences across the board, the opponent decides, numbers reconciled, calibrated judgments), not per card. Six of the owner's games (anonymised, read-only) plus three held-out fixture games nobody annotated; each case's result, Prizes, turns, ending and first player checked against its log in CI. scripts/analysis-eval.mjs runs Haiku, Haiku-gathers-Sonnet-writes and Sonnet-low over the live MCP tools on the QA account with a Sonnet judge; the QA identity is pinned (QA_USERNAME from scripts/qa-token.mjs), only reads and saving a review reach the account, and --budget is a byte-bounded ceiling with a reserve/settle ledger. Four Astra rounds. The baseline run waits on the maintainer minting the QA token. No migration. |
| 2026-10-14 | Claude Opus 5.5 | @cheyras | Phase 5 follow-up (PR #348, merged as c935c253): scripts/qa-token.mjs treats a QA_USERNAME already in .qa-account as a second pin. The QA sign-in must be that user or nothing is minted, and the line is never overwritten. Astra GOOD TO GO in one round. No migration. |
| 2026-10-17 | Claude Opus 5.5 | @cheyras | Write safety (PR #349, merged as c626e9dd; migrations 087/088 applied to production and verified before merge — RLS on, both policies present): a recycle bin for battle logs (restore and purge on the web, Deck-E and MCP); every battle-log and deck card-list write recorded with full before/after state (printings, exact-printing pins, and the guide when it moved) so revert undoes it, a deck's earlier list returning as a new version; one lock order, deck then log, for every battle-log writer and for revert; retry keys on the MCP writes a retry would double; deck_strategy previews over MCP. Owner-chosen options from #335's audit. Four Astra (Codex) rounds: binned logs holding their version, check-then-apply races, pins, the guide on an undone undo, and two lock-order deadlocks all fixed before GOOD TO GO; new writeSafety.mjs suite (15 cases) in CI. |
| 2026-10-17 | Claude Opus 5.5 | @cheyras | Phase 5 results (PR #350, merged as 46b4eaca): Two baseline runs of the analysis eval (9 cases x 3 configs, QA account, $3.85): Haiku gathering for a Sonnet writer scores 77% overall and 65% on held-out games for about 5 cents a review, against 68% for Sonnet-low and 60% for Haiku alone. P2 (mechanisms) is low in every config because battle_digest reports Weakness, cost and damage changes without their cause; that digest fix is next. The judge now sees card text; run --rejudge keeps verdicts until replaced. Three Astra rounds. No migration. |
| 2026-10-17 | Claude Opus 5.5 | @cheyras | Phase 5 fix (PR #351, merged as 51f23e47): battle_digest lists the effects in play that change the rules (Weakness or Resistance, attack costs, Tools switched off, damage, prevention, Retreat Cost, HP). They are read from the operative wording of card text, never card names, with evidence the log shows. Evidence is claimed only by the effect's owner while it is in play, and never when it is ambiguous. Six Astra rounds plus a CodeQL ReDoS fix. No migration. |
| 2026-10-17 | Claude Opus 5.5 | @cheyras | Phase 5 measurement (PR #352, merged as 3f2d7c52). The rule-effects digest raised the Jamming Tower (held-out) and Fairy Zone points in both configs, and B's held-out P2 from 50/50 to 60. A's held-out P2 did not rise, B's overall fell to 72 against 78/76, and mine-tera did not improve. It is kept provisionally pending a repeat run (decision file). Eval spend is $4.80. Three Astra rounds. |
| 2026-10-18 | Claude Opus 5.5 | @cheyras | Phase 5 follow-ups. PR #354 (e6e2f8de) moves the digest's rule-effects section after the board, under a neutral header, with quiet effects counted. Config B had stayed below its baseline (72/73 against 78/76), and the owner chose to fix it rather than revert. PR #355 (27a9ff45) records two complete B runs after the fix: overall 78/77, back in the baseline range; held-out P2 70/70, against 50/50. The section is kept; causation is not established (three changes at once), and the placement rule is a precaution. Eval spend is $7.20. |
| 2026-10-18 | Claude Opus 5.5 | @cheyras | PR #356 (aaf5aa6b): attacks whose text carries into the next turn (Shellnado Spin, Shadow Bind, Mega Brave) show as windows. The evidence is what the opponent attacked during an opponent's-next-turn window: into it, counters on it, or around it. There is no identity claim when another same-named Pokémon was in play, and the window closes when its Pokémon leaves. It targets owner-94's shellnado-dodge. Three Astra rounds. Measurement awaits the owner's go-ahead. |
| 2026-10-18 | Claude Opus 5.5 | @cheyras | Battle simulator rules checked against PTCG Live's match engine (read with REA for names only) and the official rulings (PR #357, merged as 5a7f0a5b): timed-effect windows, attack-only damage steps, Prize cards one at a time (#809), retreat payment (#2127), Checkup flip order (#867), Stadium use per card (#926), public-zone cards stay known (#787), Sudden Death for ties, deck searches may find none (#903), #668, #1823, gusts past a protected Active (ruling 2155); 99 general rulings newly pinned (532 of 1,944); the PTCG Live log replay completes 36 of 36 real games (was 18); legality notes for pasted lists in simulate_battles. Decision: 2026-10-17 Battle simulator: the core rules checked against PTCG Live's engine and the general Compendium rulings. |
| 2026-10-19 | Claude Opus 5.5 | @cheyras | Battle simulator follow-up (PR #359, merged as 56219509): checked everything #357 left open against the Advanced Player's Rulebook v3.4 and the Tournament Rules Handbook and fixed the five that broke them: tiebreaker game with 6 Prize cards (first to have fewer left wins), "during" effects end before Pokémon Checkup and "until the end of" ones last through it, Knock Outs from end-of-turn effects before Checkup, face-down setup, and the turn cap as time called (+3 turns, fewer Prize cards left wins). Decision: 2026-10-19 Battle simulator: turn timing and the end of a game follow the Advanced Player's Rulebook and the Tournament Rules. |
| 2026-10-26 | Claude Opus 5.5 | @cheyras | Phase 6, part one. PR #358 (e3c22696) adds knowledge notes: agents propose a sourced rules finding with the new propose_note tool, a reviewer holding knowledge.review approves it, and approved notes show in get_card and rules_lookup for every account. Migrations 089/090 are live on production. A note is stored only through knowledge_note_propose(), which enforces the limits and refuses personal data. Reviewer flags are computed at review time. RLS is on from 089, and a contributor can be given review. Four Astra rounds. Also fixes a time bomb in the improvement suite, where a fixed 2026-09-28 leg overflowed integer latency from 2026-10-23. |
Last updated by Claude Opus 5.5 on behalf of @cheyras -- 2026-10-26