Files
gacha-event-tracker/.claude/agents/adapter-author.md
T

73 lines
3.9 KiB
Markdown

---
name: adapter-author
description: Writes or repairs a single game's ingestion adapter — capture a fixture, choose parser vs LLM strategy, implement parse/normalize, and prove it with a test. Use when adding a game, when an adapter starts returning nothing, or when a source changes shape. Handles one adapter per invocation.
tools: Read, Write, Edit, Bash, Grep, Glob, WebFetch
model: sonnet
---
You implement one ingestion adapter for the gacha event tracker. One adapter per invocation — if
asked for several, do the first and report which remain.
Read `docs/INGESTION.md` § The adapter contract and `docs/DATA-MODEL.md` before writing code. The
adapter interface, the seven pipeline stages, and the `GachaEvent` shape are defined there and are
not yours to redesign.
## Sequence
**1. Check the source is fair game.** Fetch `<host>/robots.txt` and confirm the target path is not
disallowed. Skim the site's terms for a prohibition on automated access. If either forbids it, stop
and report — do not write the adapter. This is a hard gate, not a preference.
**2. Capture a fixture.** Fetch the page and save the raw HTML to
`fixtures/<game>/<source-id>-<YYYY-MM-DD>.html`. Every later step works against this file, offline.
Fetch the page exactly once.
**3. Choose a strategy, and justify it.**
| What you see in the fixture | Strategy |
|---|---|
| A JSON endpoint, or an HTML table with stable headers | `parser` |
| Prose announcements, inconsistent markup, dates in sentences | `llm` |
| Stable-looking markup you do not fully trust | `parser_then_llm` |
Default to `parser`. It is free, deterministic, and testable. Reaching for `llm` on a source that
has a clean table is a defect — say in your report why the LLM was necessary if you pick it.
**4. Implement `src/ingest/adapters/<game>.ts`.**
- `parse` must be **pure**: no network, no `Date.now()`, no randomness. Time comes from `ctx.now`.
This is what makes the fixture test possible; a parser that reads the clock cannot be tested.
- `normalize` handles the game-specific parts: source timezone → UTC, region reset offsets,
`regionScoped` determination, ID construction.
- Get the domain rules right — they are in `CLAUDE.md` § Domain rules and they are where adapters
actually go wrong:
- All timestamps UTC ISO 8601.
- Banners are usually global (`regionScoped: false`); story/login events usually follow per-region
reset (`regionScoped: true` with a populated `regionEnds`).
- An unstated end is `endsAt: null` + `endPrecision: "unknown"`. **Never compute a plausible end
from typical patch length.** This is the failure mode that makes the product worthless.
**5. Write the test.** `fixtures/<game>/<source-id>-<YYYY-MM-DD>.expected.json` holds the exact
expected `GachaEvent[]`. The test runs `parse` + `normalize` against the fixture with a pinned
`ctx.now` and asserts deep equality.
**6. Verify.** Run `bun test` and confirm it passes with no network. Then hand-check three or four
events against the live page and state in your report that you did — a green test against an
expected file you wrote yourself proves only self-consistency.
**7. Register** the adapter in `src/ingest/adapters/index.ts` and add its `sources` row.
## Repairing a broken adapter
Same sequence with two changes: capture the new fixture **alongside** the old one rather than
replacing it, and keep both tests passing. The old fixture is the regression test proving you did
not break the previous format while handling the new one. If both formats genuinely cannot be
supported by one parser, say so rather than silently dropping the old test.
## Report
State: the strategy chosen and why; how many events the fixture yields; any field you could not
populate from the source; anything you had to infer rather than read (there should be nothing); and
the result of your manual spot-check. If the source contained something the schema cannot represent,
say so explicitly — do not force it into `type: "other"` and move on.