# Printing Press Retro: Dub

## Session Stats
- API: Dub (link attribution platform)
- Spec source: OpenAPI 3.0.3 from https://api.dub.co
- Scorecard: 91/100 (Grade A) — post-polish; 96 pre-polish
- Verify pass rate: 100%
- Fix loops: 1 shipcheck + 1 polish
- Manual code edits: 5 (duplicate registration, dead function, auth constant, analytics subcommand wiring, CLI description)
- Features built from scratch: 8 (all transcendence features)

## Findings

### F1. Duplicate AddCommand for analytics in root.go (Bug)
- **What happened:** root.go registered `newAnalyticsCmd` twice — once from the Resources loop (OpenAPI `/analytics` path) and once from VisionSet.Analytics. Runtime panic on startup.
- **Scorer correct?** N/A — this is a build/runtime bug, not a scoring issue.
- **Root cause:** `root.go.tmpl` line 100-104 iterates `.Resources` and emits AddCommand for each. Line 122-124 conditionally adds analytics from `.VisionSet.Analytics`. When an API has a `/analytics` endpoint AND a store-backed analytics command, both paths fire. The guard at line 101 checks `PromotedResourceNames` but not VisionSet names.
- **Cross-API check:** Any API with an analytics endpoint (Stripe, Plausible, PostHog, Amplitude) would hit this. Any API where a resource name collides with a vision command name (analytics, export, import, search, sync, tail) would hit it.
- **Frequency:** API subclass — APIs with endpoints named the same as vision commands. Common names: analytics, events, search.
- **Fallback if not fixed:** Claude catches the panic on first build, removes the duplicate. But this is a 30-second delay on every affected API.
- **Worth a Printing Press fix?** Yes — template fix is trivial.
- **Inherent or fixable:** Fixable. Add VisionSet names to the Resources loop exclusion check.
- **Durable fix:** In `root.go.tmpl`, expand the guard at line 101 to also skip resources whose name matches a VisionSet command: `{{- if and (not (index $.PromotedResourceNames $name)) (ne $name "auth") (not (index $.VisionSet $name))}}`. Or build a combined exclusion set in generator.go before passing to the template.
- **Test:** Generate a CLI for an API with a `/analytics` endpoint. Verify root.go has exactly one `newAnalyticsCmd` registration.
- **Evidence:** dub-pp-cli root.go had duplicate registration at lines 95 and 106.

### F2. UpsertBatch doesn't populate resources_fts index (Bug)
- **What happened:** `sync --full` wrote data to per-resource tables via `UpsertBatch`, but FTS search returned no results because `UpsertBatch` never populated `resources_fts`.
- **Scorer correct?** Partially. Data Pipeline Integrity dropped from 10 to 7 after the manual fix because the search routing changed. The scorer is right that the data pipeline has an issue, but the original template is the root cause.
- **Root cause:** `store.go.tmpl` line 334-362: `UpsertBatch` only does `INSERT OR REPLACE INTO resources` without the FTS `DELETE/INSERT` that `Upsert` (line 165-177) performs via `upsertGenericResourceTx`.
- **Cross-API check:** Every CLI. UpsertBatch is the primary sync path.
- **Frequency:** Every API.
- **Fallback if not fixed:** Search silently returns empty results after sync. Claude may catch it during live testing, but only if the test includes a search step. High miss rate.
- **Worth a Printing Press fix?** Yes — critical. Search is a headline feature.
- **Inherent or fixable:** Fixable. Add FTS insert to UpsertBatch.
- **Durable fix:** In `store.go.tmpl`, add FTS `DELETE` + `INSERT` into `resources_fts` inside the UpsertBatch transaction loop, same pattern as `upsertGenericResourceTx`.
- **Test:** Generate any CLI, run `sync --full`, then `search <term>`. Must return results.
- **Evidence:** dub-pp-cli search returned empty after sync until manually fixed.

### F3. Search command routes to per-table FTS that's never populated (Bug)
- **What happened:** The search command queried `links_fts`, `folders_fts`, `partners_fts` — but sync (via UpsertBatch) only populated `resources_fts`. Per-table FTS tables were always empty.
- **Scorer correct?** The scorer wants domain-specific search methods. The per-table FTS tables exist and have correct schemas, but they're never written to. Scorer is partially right — the CLI should use per-table FTS, but the tables need to be populated.
- **Root cause:** `search.go.tmpl` lines 171-176 wire search to per-table FTS methods. `sync.go.tmpl` line 237 uses `UpsertBatch`, which doesn't populate per-table FTS. The per-table `Upsert{{pascal .Name}}()` methods (store.go.tmpl lines 274-327) DO handle per-table FTS, but sync never calls them.
- **Cross-API check:** Every CLI with FTS-enabled tables.
- **Frequency:** Every API.
- **Fallback if not fixed:** Same as F2 — search silently fails.
- **Worth a Printing Press fix?** Yes. Two options: (a) sync uses per-table Upsert methods instead of generic UpsertBatch, or (b) UpsertBatch populates per-table FTS. Option (a) is more correct but requires sync to know about table names. Option (b) is simpler.
- **Inherent or fixable:** Fixable. Best fix is making sync.go.tmpl call per-table upsert methods when the resource matches a known table, falling back to UpsertBatch for unknown resources.
- **Durable fix:** In `sync.go.tmpl`, add a dispatch switch: for resources with per-table FTS, call `db.Upsert{{pascal resource}}()` per item. For all others, continue with `UpsertBatch`. This also populates per-table FTS correctly.
- **Test:** Generate CLI, sync, search via per-table FTS. Must return results.
- **Evidence:** dub-pp-cli search returned "no such column" errors from empty FTS tables.

### F4. retryAfter has no maximum cap (Bug)
- **What happened:** Dub's API returned a Retry-After header with a far-future timestamp, causing `retryAfter()` to return ~1.2 million hours. Sync hung indefinitely.
- **Scorer correct?** N/A — not a scored dimension.
- **Root cause:** `client.go.tmpl` lines 547-562: `retryAfter()` parses Retry-After as seconds or HTTP date but applies no maximum. Both `time.Duration(seconds) * time.Second` and `time.Until(t)` can return enormous values.
- **Cross-API check:** Any API that sends Retry-After headers with large values or dates. Common with rate-limited APIs.
- **Frequency:** Most APIs — any API with rate limiting.
- **Fallback if not fixed:** CLI hangs during sync. User must Ctrl-C. Claude may not catch it because the hang happens during runtime, not build.
- **Worth a Printing Press fix?** Yes — simple, high impact.
- **Inherent or fixable:** Fixable. Cap at 60 seconds.
- **Durable fix:** In `client.go.tmpl`, add `const maxRetryWait = 60 * time.Second` and cap both the integer and date parsing paths.
- **Test:** Mock a 429 response with `Retry-After: 999999`. Verify wait is capped at 60s.
- **Evidence:** dub-pp-cli sync hung for "1279358h2m3s" on events endpoint.

### F5. Dogfood checks client.go for "Bearer " but auth is in config.go (Scorer bug)
- **What happened:** Dogfood reported "auth protocol mismatch: uses unknown prefix" even though the CLI correctly implements Bearer auth. Had to add a dummy constant `authScheme = "Bearer "` to client.go.
- **Scorer correct?** No. The CLI is correct. Dogfood checks the wrong file.
- **Root cause:** `dogfood.go` lines 459-474 reads `client.go` and searches for `"Bearer "` or `"Bot "` string literals. But the generated client delegates auth to `config.AuthHeader()`, which is in `config.go`. The string `"Bearer "` only appears in config.go.tmpl (lines 115, 127).
- **Cross-API check:** Every CLI using Bearer auth (most APIs).
- **Frequency:** Every API with bearer_token auth.
- **Fallback if not fixed:** Claude adds a dummy constant to client.go. Works but is a workaround for a broken check.
- **Worth a Printing Press fix?** Yes — fix dogfood to also check config.go.
- **Inherent or fixable:** Fixable. Expand the file search in dogfood.go to include config.go.
- **Durable fix:** In `dogfood.go` line 459, read both `client.go` AND `config.go`. Check both sources for the auth scheme string.
- **Test:** Generate a Bearer auth CLI. Run dogfood without the dummy constant. Must report MATCH.
- **Evidence:** dub-pp-cli dogfood reported FAIL until authScheme constant was added to client.go.

### F6. formatCompact emitted but never called (Template gap)
- **What happened:** `helpers.go` contained dead function `formatCompact`. Dogfood caught it. Had to manually delete.
- **Scorer correct?** Yes — Dead Code check is correct.
- **Root cause:** `helpers.go.tmpl` lines 693-705 unconditionally emit `formatCompact`. Zero call sites exist in any template.
- **Cross-API check:** Every CLI.
- **Frequency:** Every API.
- **Fallback if not fixed:** Claude deletes it during shipcheck. Reliable but wasteful — 5-10 seconds per generation.
- **Worth a Printing Press fix?** Yes — remove from template or gate behind a conditional.
- **Inherent or fixable:** Fixable. Remove the function from the template, or gate it behind a `{{if .UsesCompactNumbers}}` conditional.
- **Durable fix:** Remove `formatCompact` from `helpers.go.tmpl`. If a future feature needs it, add it back with a gate.
- **Test:** Generate any CLI. Verify helpers.go has no `formatCompact` function. Run `go vet` — no dead code.
- **Evidence:** dub-pp-cli helpers.go had unused formatCompact, caught by dogfood.

## Prioritized Improvements

### P1 — High priority
| Finding | Title | Component | Frequency | Fallback Reliability | Complexity |
|---------|-------|-----------|-----------|---------------------|------------|
| F2 | UpsertBatch missing FTS | Generator: store.go.tmpl | Every API | Low — search silently fails | Small |
| F3 | Search routes to dead FTS | Generator: search.go.tmpl + sync.go.tmpl | Every API | Low — search silently fails | Medium |
| F4 | retryAfter no cap | Generator: client.go.tmpl | Most APIs | Low — CLI hangs | Small |
| F5 | Dogfood checks wrong file | Scorer: dogfood.go | Every bearer API | Medium — workaround exists | Small |

### P2 — Medium priority
| Finding | Title | Component | Frequency | Fallback Reliability | Complexity |
|---------|-------|-----------|-----------|---------------------|------------|
| F1 | Duplicate AddCommand | Generator: root.go.tmpl | API subclass (resource name = vision name) | High — caught on build | Small |
| F6 | formatCompact dead code | Generator: helpers.go.tmpl | Every API | High — caught by dogfood | Small |

### Skip
*No findings skipped — all are systemic.*

## Work Units

### WU-1: Fix FTS indexing in UpsertBatch and sync routing (from F2, F3)
- **Goal:** Search returns results after sync for every generated CLI
- **Target:** Generator templates: `internal/generator/templates/store.go.tmpl` and `internal/generator/templates/sync.go.tmpl`
- **Acceptance criteria:**
  - positive: Generate CLI, sync, search — returns results from both generic and per-table FTS
  - negative: Per-table FTS tables contain data after UpsertBatch
- **Scope boundary:** Does not change the FTS schema or add new FTS tables
- **Dependencies:** None
- **Complexity:** Medium

### WU-2: Cap retryAfter at 60 seconds (from F4)
- **Goal:** CLI never waits more than 60 seconds on a rate limit response
- **Target:** Generator template: `internal/generator/templates/client.go.tmpl` lines 547-562
- **Acceptance criteria:**
  - positive: 429 with Retry-After: 999999 waits 60s, not 999999s
  - negative: 429 with Retry-After: 5 still waits 5s
- **Scope boundary:** Does not change retry count or backoff strategy
- **Dependencies:** None
- **Complexity:** Small

### WU-3: Fix dogfood Bearer detection to check config.go (from F5)
- **Goal:** Dogfood correctly detects Bearer auth when constructed in config.go
- **Target:** Scorer: `internal/pipeline/dogfood.go` lines 459-474
- **Acceptance criteria:**
  - positive: Bearer auth CLI passes dogfood without dummy constant in client.go
  - negative: Non-bearer auth CLIs are not false-positively matched
- **Scope boundary:** Does not change scorecard auth scoring
- **Dependencies:** None
- **Complexity:** Small

### WU-4: Deduplicate VisionSet vs Resource AddCommand in root.go (from F1)
- **Goal:** No duplicate AddCommand registrations when resource name matches a vision command
- **Target:** Generator template: `internal/generator/templates/root.go.tmpl` lines 100-104
- **Acceptance criteria:**
  - positive: API with /analytics endpoint generates one newAnalyticsCmd registration
  - negative: API without /analytics still gets VisionSet analytics
- **Scope boundary:** Does not change promoted command logic
- **Dependencies:** None
- **Complexity:** Small

### WU-5: Remove dead formatCompact from helpers template (from F6)
- **Goal:** No dead functions emitted by default
- **Target:** Generator template: `internal/generator/templates/helpers.go.tmpl` lines 693-705
- **Acceptance criteria:**
  - positive: Generated helpers.go has no formatCompact
  - negative: go vet reports no unused functions in helpers.go
- **Scope boundary:** If formatCompact is needed later, add it back with a conditional gate
- **Dependencies:** None
- **Complexity:** Small

## Anti-patterns
- **Workaround over root cause:** Adding `authScheme = "Bearer "` to client.go is a workaround for a dogfood bug. The fix should be in dogfood.go.
- **Silent failure:** FTS search returning empty results without error is the worst kind of bug — it looks like "no data" rather than "broken index."

## What the Printing Press Got Right
- **OpenAPI parsing:** 47 operations across 14 resources parsed correctly on first try. Zero path validity failures.
- **Auth detection:** Bearer token auth inferred correctly from spec security scheme. Doctor validates credentials.
- **Quality gates:** All 7 gates passed on first pull (go mod tidy, go vet, go build, binary, --help, version, doctor).
- **Agent-native output:** --json, --select, --csv, --compact, --dry-run, --agent all worked correctly across all commands.
- **Scorecard 96/100 pre-polish:** Only 4 points lost, all in domain correctness tier.
- **Store schema generation:** 15 tables with appropriate FTS indexes generated from spec analysis.
