Hydrate pseudo columns on card output - #207
Conversation
Cards in the built-in Maybe, Not Now, and Done lanes come back from the API with an empty column object, leaving JSON consumers without a usable column id or name (fixes basecamp#202). Infer the lane from the card payload when column.id is empty — closed maps to Done, postponed to Not Now, published to Maybe — and emit the same {id, name, kind, pseudo} shape that column list already prints. Drafts have no lane upstream and are left untouched, as are cards with a real column. Applied to card show/list, search, and board stream/closed/postponed.
There was a problem hiding this comment.
Pull request overview
Hydrates missing card column metadata for Fizzy’s built-in lanes, resolving #202 for JSON consumers.
Changes:
- Infers Maybe, Not Now, and Done pseudo-columns.
- Applies hydration across card, search, and board outputs.
- Adds unit and command-level coverage.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
internal/commands/pseudocolumns.go |
Implements pseudo-column inference and hydration. |
internal/commands/pseudocolumns_test.go |
Tests inference, exclusions, lists, and card commands. |
internal/commands/card.go |
Hydrates card list and show output. |
internal/commands/board.go |
Hydrates closed, postponed, and stream output. |
internal/commands/search.go |
Hydrates search results. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
jeremy
left a comment
There was a problem hiding this comment.
Fresh pass over 4911fde, checked against the fizzy model rather than the CLI alone.
Correctness. The inference matches upstream. cards/_card.json.jbuilder emits column only if card.column, so a card in any built-in lane arrives with no column at all; closed? is "a closure exists", postponed? is open? && published? && not_now.present?, and awaiting_triage? is active-with-no-column. That is exactly closed → done, postponed → not-now, published → maybe, drafted untouched. The closed-over-postponed precedence can't actually diverge (postponed? already requires open?), so it's a harmless belt.
Two shapes reach hydrateCardColumn: the SDK's non-pointer Column field survives normalizeAny as {"id":"","name":"","created_at":""} (omitempty doesn't drop a struct), while the --all paths go through jsonAnySlice over the raw pages, where the key is absent entirely. The nil-map read handles both, but no test covers the absent-key case — one more t.Run in TestHydrateCardColumn with no column key would keep the --all path honest.
Consistency. The mutation outputs still emit the raw column: card create (a fresh published card lands in Maybe), card update, card close, card reopen, card postpone, card resume, card untriage and card move all go through printMutation(normalizeAny(data)) without hydration. So fizzy card close 5 --json reports column.id: "" while fizzy card show 5 --json says done a second later. Same one-liner at those sites would close the gap; fine as a follow-up if you'd rather keep this PR to the read paths, but it's small enough to fold in.
One semantic note for the doc comment. close upstream doesn't clear column, so a card closed from a real column keeps that column in the API and is not hydrated — column.id == "done" is therefore not the way to detect closure; closed stays the signal. Worth a sentence on inferPseudoColumn so a consumer doesn't read column.id as the lane of record.
Verification. go test ./..., go test -race -count=1 ./internal/..., go vet, gofmt -l, go mod tidy -diff and make surface-check are all clean on this branch locally. The GitHub workflow runs for this head are sitting in action_required (first-time contributor), so the checks haven't run there yet; that's a maintainer button, not something I'm approving from here.
Fixes #202 as scoped; nothing here needs a change before merge, and I haven't pushed to the branch.
Fixes #202
Problem
Cards sitting in Fizzy's built-in lanes — Maybe, Not Now, Done — have no real column, so the API returns an empty
columnobject ({"id":"","name":""}). The CLI passed that through verbatim, leaving--jsonconsumers without a usable column id or name:Upstream behavior confirmed in basecamp/fizzy#2596 and basecamp/fizzy#3017; the API is unlikely to change soon, so this hydrates client-side.
Fix
When a card payload arrives with an empty
column.id, infer its lane from fields the API already provides and emit the same pseudo-column shape thatfizzy column listprints:closed == true→ Done (done, kindclosed)postponed == true→ Not Now (not-now, kindnot_now)maybe, kindtriage)The inference is safe because the states are mutually exclusive upstream: closing a card destroys its not_now record, and postponing clears its column. Hydrated ids round-trip back into the CLI (
fizzy card column <n> --column maybe).Applied to:
card show,card list,search, andboard stream/closed/postponed.Testing
internal/commands/pseudocolumns_test.go: per-lane inference, closed-over-postponed precedence, draft/real-column/unknown-payload no-ops, list handling, plus command-level tests forcard showandcard list.make fmt-check vet tidy-checkandmake race-test(includesTestSurfaceSnapshot) pass; no CLI surface change.Summary by cubic
Hydrates pseudo columns on card output so JSON always includes a usable column id/name. Previously, cards in built-in lanes (Maybe, Not Now, Done) returned an empty column; now the CLI infers the lane and emits the same pseudo-column shape as column list.
done, kindclosed); postponed → Not Now (not-now, kindnot_now); published → Maybe (maybe, kindtriage). Drafts and cards with a real column are unchanged.card show,card list,search,board stream,board closed,board postponed.fizzy card column <n> --column maybe.column.idfor built-in lanes.Written for commit 4911fde. Summary will update on new commits.