toko-feed-cli 0.3.2

Operator CLI for Toko Feed canister ingestion and catalog queries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# Toko Feed

> Durable, provider-corroborated collectible data on the Internet Computer.

Toko Feed is a standalone canister that collects provider evidence, preserves
price history, and turns reviewed records into stable Toko identities. It
currently supports Pokémon data from JustTCG, TCGdex, and Scrydex, with an
embedded IcyDB database and a typed operator CLI.

Provider data is **evidence, not truth**. Ingestion never silently creates or
overwrites canonical Pokémon sets or cards.

## ✨ Highlights

- **Self-contained canister** — HTTPS outcalls, scheduling, retries,
  checkpoints, and storage live together.
- **Multi-provider evidence** — provider wire formats stay isolated and are
  retained without being flattened into a universal schema.
- **Explicit reconciliation** — corroborated records can become provisional
  truth; ambiguous records remain queued for human review.
- **Queryable game mechanics** — Pokémon types, HP, stages, retreat costs,
  attacks, costs, abilities, weaknesses, resistances, and rules are normalized
  into typed relations rather than buried in JSON.
- **Attributed prices** — every price keeps its provider, item, variant,
  currency, API version, and observation time.
- **Upgrade-safe ingestion** — durable jobs resume from bounded checkpoints
  and recover interrupted runs.
- **Safe operations** — credentials, response bodies, and raw transport errors
  never appear on public or operational surfaces.

## 🚀 Quick start

The repository pins Rust 1.97.1. The full build also expects the
`wasm32-unknown-unknown` target, the current `icp` CLI, `didc`, and
`candid-extractor`.

```bash
rustup target add wasm32-unknown-unknown
make canister
make install
toko-feed --help
```

Build artifacts:

- `target/wasm32-unknown-unknown/release/toko_feed.wasm`
- `crates/toko-feed/toko-feed.did`

### Run locally

Create an ignored credential file. JustTCG is required by canister
configuration; Scrydex credentials are optional but must be supplied as a
key/team pair. Bootstrap itself is cache-only and makes no provider requests.

```bash
cp .env.example .env.local
chmod 600 .env.local
# Edit .env.local.

make local-ready
toko-feed status
```

`make local-ready` starts the managed local network, deploys the canister,
disables automatic polling, and replays every retained development provider
cache without network access. Cached evidence lives under ignored
`source/providers/` directories; reviewed canonical snapshots live under
tracked `data/`.

> **Caution:** `make local-reset` reinstalls the local canister and erases its
> existing data before replaying the provider cache.

## 🧭 How the data flows

```text
Provider APIs ──> current source rows ──> immutable observations
                   reconciliation ──> canonical Toko rows ──> public queries
```

The boundaries are deliberate:

1. Collection, set, card, and sealed source rows retain current, replaceable
   provider records. Set and card evidence also carries its reconciliation
   state: `pending`, `mapped`, or `rejected`.
2. `SourceObservation` and `PriceObservation` retain immutable historical
   evidence.
3. `PokemonSet`, `PokemonCard`, `PokemonCardPrinting`, and the normalized
   Pokémon metadata entities contain curated Toko truth. Their lifecycle is
   `provisional``verified``locked`.

Safe reconciliation accepts only corroborated, non-conflicting candidates and
creates provisional rows. Explicit reconciliation resolves single-source
claims, conflicts, and physical printing suffixes such as `50a`/`50b`. Locking
is irreversible; additional matching provider evidence may still be attached
to a locked row. Sealed products remain provider-derived projections until a
dedicated canonical reconciliation model is introduced.

Canonical IDs are Toko-owned ULIDs generated before persistence, so a snapshot
can restore them exactly instead of accepting replacement database IDs.
Collection IDs use explicit, namespace-stable assignments; unknown provider
categories are rejected before storage. Provider keys are mappings, never
canonical identities.

Prices are never canonical values. Current prices are derived from mapped
sources, while history remains a sequence of attributed observations. Values
in different currencies are not compared implicitly.

Gameplay metadata follows the same evidence boundary. Each provider adapter
preserves its wire model and owns its normalization into shared Pokémon
vocabulary. Canonical metadata is stored as relations: card types, attacks,
typed attack-cost counts, abilities, type modifiers, and rules. Printed damage
such as `30+`, `30×`, and `40-` is represented by a numeric amount and an enum
operation, so it remains searchable without discarding its rules meaning.
IcyDB 0.221 does not yet expose typed continuation cursors, so type lookup uses
a deterministic materialized search key derived from the type and metadata
ULID; the enum remains the canonical value.

Collections share only the generic `Collection` catalogue root. Their storage
branches remain typed and separate: Pokémon uses `PokemonSet`, `PokemonCard`,
printings, sealed products, and Pokémon mechanics; Magic uses `MagicSet`, a
rules-level `MagicCard`, `MagicCardPrinting`, and ordered `MagicCardFace` rows.
No Magic row can be stored in a Pokémon table. Cross-collection search will be
a read model over these branches rather than a universal table full of
collection-specific nullable columns. The extension rules are documented in
[the canonical data model](docs/architecture/data-model.md).

## 🔌 Providers

| Provider | Integration | Credentials | Bounds |
| --- | --- | --- | --- |
| JustTCG v1 | Collections, Pokémon sets, cards, sealed products, and prices | API key | Card pages contain at most 10 records |
| TCGdex v2 English | Pokémon set indexes and card details | None | One provider request per canister call |
| Scrydex Pokémon v1 English | Pokémon expansions and cards, without price expansions | API key + team ID | Card pages contain at most 10 records |
| Scrydex Magic v1 | Magic expansions and cards, preserving mechanics and print evidence | API key + team ID | Card pages contain at most 10 records |

Each adapter owns request construction, wire models, compatibility aliases,
sanitized fixtures, and provider-local normalization under
`crates/toko-feed/src/api/<provider>/`. Tests never call live providers.

Provider acquisition has no adapter-named public methods. The canister exposes
`toko_feed_ingest_provider_set` and `toko_feed_ingest_provider_cards`; both take
a text `provider` field and dispatch internally. Canonical names are
`just-tcg`, `tcgdex`, and `scrydex`; ASCII case, hyphens, underscores, and
spaces are normalized. Unique leading fragments such as `just` and `scry` are
accepted without an alias table. Unknown, ambiguous, or suspicious names fail
before an HTTP request is made.

The current development corpus uses Expedition, Aquapolis, and Skyridge for
JustTCG and an Aquapolis comparison across all three providers. Disagreement
is retained as normal curation input instead of being hidden or rewritten.

Magic: The Gathering evidence can be acquired from JustTCG and Scrydex without
entering Pokémon tables or prematurely creating canonical Magic truth. Current
set and card payloads retain each provider's wire format in collection-scoped
`ProviderRecord` rows plus immutable observations; future Magic reconciliation
will project those records into the separate rules, printing, face, and set
hierarchy.

## 🧑‍💻 Operator workflow

### 1. Acquire or replay evidence

```bash
toko-feed bootstrap
toko-feed provider set just-tcg aquapolis-pokemon --all
toko-feed provider set tcgdex ecard2 --all
toko-feed provider set scry ecard2 --all
toko-feed provider set just-tcg "The Dark" \
  --collection magic --all
toko-feed provider set scry DRK \
  --collection magic --all
```

`bootstrap` replays all retained provider data and never calls an API. Use a
provider command with `--refresh` to update metadata and fetch missing card
positions. JustTCG selectors may be exact provider IDs or unambiguous display
names. Collection arguments use centralized, Unicode-safe matching:
`magic` is an unambiguous leading fragment of `Magic: The Gathering`; the full
slug and display name also work. Every accepted form resolves to the canonical
`magic-the-gathering` slug. There is no collection alias table to maintain. An
ambiguous fragment is rejected and must be made more specific. Scrydex uses
its provider expansion ID (`DRK` for The Dark). Complete cached card records
are not downloaded again.

Operator matching folds Latin diacritics and Unicode compatibility forms to
ASCII, so `poke`, `Pokemon`, and `Pokémon` all resolve to the canonical
`pokemon` collection. Invisible and non-Latin confusable characters remain
invalid; provider response bodies retain their original bytes.

Provider arguments follow the same rule: `scry` and `just` work as unique
leading fragments of `scrydex` and `just-tcg`, without a provider alias table.

### 2. Inspect the canister

```bash
toko-feed status
toko-feed status --collection pokemon --all
toko-feed status --collection magic --set the
toko-feed status --collection pokemon --set aquapolis --json
toko-feed scheduler
toko-feed runs --limit 100
toko-feed logs --all
toko-feed sources collections list --all
toko-feed sources sets list --all
toko-feed sources cards list --all
toko-feed sources sealed list --all
```

`status` is the bounded reconciliation dashboard. Each collection/set row
separates canonical card totals and lock percentage from provider set/card
evidence, reports per-provider coverage, and supplies the highest-priority
typed next action. Collection and set filters use the same exact-or-unique-
prefix matching as acquisition. Provider-reported totals remain attributed
evidence and are never used as the denominator for canonical lock progress.
Human-readable text tables are the default. Add `--json` to any command for
the complete script-friendly JSON representation.

### 3. Reconcile canonical truth

```bash
toko-feed reconcile sets plan --all
toko-feed reconcile sets accept-safe \
  pokemon-set:v1:pokemon:aquapolis:2003-01-15

toko-feed reconcile cards plan --set <SET_ULID> --all
toko-feed reconcile cards accept-safe --set <SET_ULID> --all
```

Safe card acceptance processes bounded pages and leaves conflicts,
single-source candidates, and suffixed printings pending. Use the explicit
`accept` and `accept-printing` commands for reviewed exceptions; run the
relevant `--help` command for their required fields.

Reviewed exceptions use the same reconciliation boundary:

```bash
toko-feed sources sets get <SOURCE_ULID>
toko-feed reconcile sets accept --source <SOURCE_ULID> \
  --name "Base Set" --release-date 1999-01-09 --active
toko-feed sets verify <SET_ULID> --revision <REVISION>
toko-feed sets lock <SET_ULID> --revision <REVISION>

toko-feed sources cards get <SOURCE_ULID>
toko-feed reconcile cards accept --source <SOURCE_ULID> --set <SET_ULID> \
  --name "Alakazam" --collector-number "1/102" --rarity "Rare Holo"
toko-feed reconcile cards accept-metadata \
  --card <CARD_ULID> --source <SOURCE_ULID>
toko-feed cards verify <CARD_ULID> --revision <REVISION>
toko-feed cards lock <CARD_ULID> --revision <REVISION>
```

Every canonical write uses the latest revision. A stale revision returns
`CanonicalConflict` instead of overwriting another curator's work.
Metadata acceptance currently promotes one reviewed, mapped TCGdex or Scrydex
payload into a provisional normalized aggregate and records the exact source
row and normalizer version. Another provider can be attached when its complete
normalized projection agrees exactly; contradictions remain source evidence
and return `CanonicalConflict`. Field-by-field consensus is a later
reconciliation step, and raw evidence is always retained unchanged.

### 4. Query canonical data

```bash
toko-feed collections list --all
toko-feed sets list --all
toko-feed sets get <SET_ULID>
toko-feed cards list --all
toko-feed cards get <CARD_ULID>
toko-feed cards by-type fire --all
toko-feed cards prices <CARD_ULID> --limit 100
toko-feed sealed list --all
toko-feed backup
toko-feed restore
```

Without `--all`, list commands return one page and a continuation. Entity
lists use `next_after`; run, log, and price history use the compound
`next_before` cursor. Automatic pagination detects repeated cursors and is
bounded by `--max-pages`.

`backup` writes provider-independent canonical truth to
`data/canonical.json`; provider evidence, observations, scheduler state, and
credentials are deliberately excluded. `restore` validates that snapshot and
replays it in bounded dependency order. Repeating a matching restore is a
no-op, while a conflicting ID or canonical key fails rather than overwriting
curated truth. `make local-ready` and `make local-reset` restore canonical data
before replaying the retained provider cache.

The move to application-owned canonical ULIDs changes the schema-v1 insert
policy for canonical entities. Existing development deployments created before
this change should run `make local-reset` once; the tracked snapshot restores
their canonical IDs and fields after reinstall. This is intentionally not
presented as an in-place production migration while the schema is still in its
reinstall-first development phase.

Global connection options must precede the command:

```bash
toko-feed --environment ic --canister <PRINCIPAL> \
  --identity operator status
```

Use `--identity-password-file` for protected non-interactive identities.
Human-readable text is the default; `--json` selects raw JSON and
`--json --compact` puts it on one line. Output flags may appear after the
command. The CLI deliberately has no API-key argument; local credentials are
read by the configuration script from `.env.local`.

### CLI hierarchy

```text
toko-feed
├── bootstrap
├── backup
├── restore
├── status [--collection <COLLECTION>] [--set <SET>] [pagination]
├── scheduler
├── runs [pagination]
├── logs [pagination]
├── collections list|get
├── sets list|get|verify|lock
├── cards list|get|by-type|verify|lock|prices
├── sealed list|get|prices
├── sources
│   ├── collections list|get
│   ├── sets list|get|reject
│   ├── cards list|get|reject
│   └── sealed list|get
├── provider set <PROVIDER> <SET> [--collection <COLLECTION>] [--records <COUNT>|--all]
└── reconcile
    ├── sets plan|accept-safe|accept
    └── cards plan|accept-safe|accept|accept-printing|accept-metadata
```

Canonical resources, provider evidence, acquisition, and reconciliation each
have one top-level home. Collection and sealed sources are read-only because
they do not yet have a pending/rejected reconciliation lifecycle.

## ⏱️ Scheduling and reliability

The canister maintains three durable jobs: collections, Pokémon sets, and
Pokémon cards. Successful runs recur after 24 hours, six hours, and the
configured card cadence respectively; the default card cadence is 15 minutes.

Jobs use one-shot timers so intervals cannot overlap. A run is recorded before
its first HTTPS await, each page is checkpointed, and interrupted work is
recovered explicitly after upgrade. Failures use capped exponential backoff
with jitter, separate from the normal success cadence.

Every response and replayed payload passes a non-destructive integrity screen
for unsafe Unicode, mixed-script confusables, encoded payload markers, and
excessive combining or repetition patterns. Findings produce fixed,
credential-free warning categories while the original valid evidence remains
unchanged.

## 🔐 Security notes

- Provider HTTPS requests are made only by the canister; the CLI has no
  provider transport.
- Credentials are controller-only configuration and are never returned by
  status, query, debug, or error surfaces.
- Logs contain typed codes and fixed bounded messages—not request headers,
  provider bodies, credentials, or raw transport diagnostics.
- Public methods expose bounded, attributed projections rather than raw
  provider JSON.
- Provider access does not grant redistribution rights. Review each active
  provider plan and terms before exposing a production feed.

## 🗄️ Database inspection

Install the operator CLI and the matching IcyDB 0.221.6 CLI:

```bash
make install-tools
```

Toko Feed exposes controller-only, read-only SQL and schema inspection:

```bash
icydb sql --environment local --canister toko-feed
icydb sql -e local -c toko-feed --sql "SHOW ENTITIES"
icydb sql -e local -c toko-feed \
  --sql "SELECT * FROM PokemonSetSource LIMIT 20"
icydb sql -e local -c toko-feed \
  --sql "SELECT * FROM PokemonCardType LIMIT 20"
icydb schema show toko-feed --environment local
```

The ICP identity used by `icydb` must control the canister. SQL mutation and
DDL endpoints are not exposed.

## 🏗️ Repository layout

| Path | Responsibility |
| --- | --- |
| `crates/toko-feed` | Standalone canister, provider adapters, canonical rules, public protocol DTOs, and embedded schema |
| `crates/toko-feed/src/canister` | Authorization, HTTPS execution, bounded ingestion, durable jobs, and Candid endpoints |
| `crates/toko-feed/src/schema` | Stable IcyDB entities for sources, observations, canonical data, jobs, and logs |
| `crates/toko-feed-cli` | Host-only operator executable and typed `icp` process boundary |
| `crates/toko-feed/toko-feed.did` | Authoritative public Candid interface |
| `data/` | Tracked curated canonical snapshots |
| `source/providers/` | Ignored replayable provider evidence |
| `icp.yaml` | Local and IC project lifecycle configuration |

The canister is the product boundary. There is no separate schema canister,
Canic dependency, or Toko repository dependency. A future fleet can consume
the compiled Wasm as an opaque role; the compatibility boundary is documented
in [Canic fleet integration](docs/architecture/canic-fleet.md).

## 🧪 Development

Run the complete required check suite:

```bash
make check
```

This runs formatting, Clippy, tests, rustdoc, the release Wasm build, and strict
equality between the compiled interface and
[`toko-feed.did`](crates/toko-feed/toko-feed.did).

Useful focused targets:

```bash
make fmt
make clippy
make test
make docs
make wasm
make candid-check
```

CI uses `make ci`, which starts with a clean target directory and removes only
Toko Feed-owned generated artifacts. Run `make help` for all targets.

## 🚢 Deployment and releases

Deploy the same standalone canister to the IC with the active ICP identity:

```bash
make ic-deploy
```

Deployment does not copy local credentials or trigger paid imports. Configure
production credentials through a protected controller workflow and ensure the
canister has enough cycles for HTTPS outcalls.

Release notes live in [CHANGELOG.md](CHANGELOG.md). From a clean branch, after
committing non-release work:

```bash
make release-patch  # or release-minor / release-major
make publish-dry-run
make publish
```

Release targets run the full checks, update workspace versions, create an
annotated tag, and push the release atomically. Publication uploads
`toko-feed` before `toko-feed-cli`.

## 📄 License

Licensed under the [MIT License](LICENSE).