Skip to content

Hydrate pseudo columns on card output - #207

Merged
jeremy merged 3 commits into
basecamp:masterfrom
tahadx:fix/202-pseudo-column-output
Oct 24, 2026
Merged

jeremy merged 3 commits into
basecamp:masterfrom
tahadx:fix/202-pseudo-column-output

Conversation

@tahadx

@tahadx tahadx commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

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 column object ({"id":"","name":""}). The CLI passed that through verbatim, leaving --json consumers without a usable column id or name:

"column": { "created_at": "", "id": "", "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 that fizzy column list prints:

  • closed == true → Done (done, kind closed)
  • postponed == true → Not Now (not-now, kind not_now)
  • otherwise published → Maybe (maybe, kind triage)
  • drafts belong to no lane upstream and are left untouched
"column": { "id": "maybe", "name": "Maybe?", "kind": "triage", "pseudo": true }

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, and board stream/closed/postponed.

Testing

  • New unit tests in 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 for card show and card list.
  • make fmt-check vet tidy-check and make race-test (includes TestSurfaceSnapshot) 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.

  • Inference: closed → Done (done, kind closed); postponed → Not Now (not-now, kind not_now); published → Maybe (maybe, kind triage). Drafts and cards with a real column are unchanged.
  • Applied to: card show, card list, search, board stream, board closed, board postponed.
  • Output shape example: {"id":"maybe","name":"Maybe?","kind":"triage","pseudo":true}; supports round-trip via fizzy card column <n> --column maybe.
  • Safety: upstream states are mutually exclusive; closed takes precedence over postponed.
  • Migration: none. JSON consumers can rely on column.id for built-in lanes.

Written for commit 4911fde. Summary will update on new commits.

Review in cubic

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.
Copilot AI balanced review requested due to automatic review settings August 23, 2026 16:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 jeremy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jeremy

jeremy commented Oct 24, 2026

Copy link
Copy Markdown
Member

🤖 Thank you for picking this up, @tahadx! Showing a usable column for cards in Maybe?, Not Now and Done makes card output much easier to work with.

The branch was behind master, so I've merged master into it (23555f9). It applied cleanly, and CI is green on the new head. The new tests pass alongside the rest of the suite.

I checked inferPseudoColumn against the card JSON Fizzy serves and the model behind it:

  • Not Now holds. Postponing a card sends it back to triage (column: nil) before it creates the not_now record, and postponed is only true for an open, published card. A postponed card never has a real column, and it is never closed.
  • Maybe? holds. A published, open, unpostponed card with no column is exactly what Fizzy calls awaiting triage. Leaving drafts alone is right too.
  • Done has one gap. Closing a card destroys its not_now, but it doesn't clear column_id. A card that was in "Development" when it was closed keeps that column, and the API still returns column: {id: …, name: "Development"} for it. hydrateCardColumn returns early on a real column id, so fizzy card show on that card still says Development, while Fizzy itself shows it in Done. It's the same as on master, so nothing regresses, but it's the case this change seems meant to cover. Would you be up for checking closed before the real-column early return, with a test for a closed card that still has a column? If you'd rather keep the API's column for closed cards, a line in the doc comment saying so would work too.

One optional follow-up, not for this PR: the mutation responses (card create, update, move and the like) print the card without hydration, so they can still show an empty column.

Thanks again!

Closing a card doesn't clear its column, so the API still returns the
column a card was closed from, and hydrateCardColumn kept it because it
only looked at cards with an empty column. Check closed first: a closed
card is in Done, a card with a real column keeps it, and a card without
one is Not Now when postponed or Maybe? when published. Drafts stay
untouched.
@jeremy

jeremy commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

🤖 Thanks again, @tahadx. We've added closed-card handling on top of your commits (89d2f1c), so your commits are unchanged.

Closing a card doesn't clear its column, so the API still returns the column a closed card was closed from. hydrateCardColumn now checks closed first. The order is:

  1. A closed card is in Done, whatever its column.
  2. A card with a real column keeps it.
  3. A card with no column is Not Now if it's postponed, or Maybe? if it's published.
  4. Drafts are left untouched.

Since closed is now handled before inferPseudoColumn runs, that function only deals with Not Now and Maybe?. New tests cover a closed card with a real column (directly, and through card show and card list) and a closed card with no column key, alongside your empty-column case. The real-column tests fail without the change. make check passes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approved

The focused hydration logic integrates consistently with normalized output, has targeted test coverage, and has no identified blocking issues.

No open issues 🎉

Review effort: Balanced

@jeremy
jeremy merged commit 2bd7c47 into basecamp:master Oct 24, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Was it a design decision to not give default columns id's?

3 participants