Model the special-cased search branches and person_minimal's guaranteed keys

Merged
JE
2mo ago
+2647-217

Two spec-shape corrections, one commit per issue, each carrying its own full regeneration across all six SDKs. Both were audited against bc3 at exactly the provenance pin (all four special-cased search branches read from the object store at the pinned revision, every fact re-verified before encoding).

Closes #651. Closes #659.

SearchResult models what the four special branches actually emit (#651)

bc3's search rendering special-cases four result types (searches/attachment, chats/lines/line, kanban/lists/list, gauges/needles/needle — exhaustively enumerated from the dispatch helper; there is no fifth). Only the attachment branch violates the old @required set; the others contribute optional keys.

  • Demoted id, title, type, url, app_url from @required — the attachment branch emits none of them. content/description stay required: the show template nil-overwrites them after every branch partial.
  • Added 26 optional members: the attachment branch's 10 file keys (width/height are float-spelled ints upstream → the existing *types.FlexInt nullable pattern), kanban list keys (on_hold reuses CardColumnOnHold, whose 8 subkeys match the branch emission exactly), gauge-needle keys, chat-line keys, and common-envelope subscription_url.
  • attachments got its own element shape. The old doc comment claimed top-level attachments always duplicates the rich-text companion array — falsified by chat upload lines, which build a bespoke inline {title, url, filename, content_type, byte_size, download_url} aggregate and have no rich_text_content, so the overwrite never fires and the bespoke shape reaches the wire. It misses RichTextAttachment's required id/sgid/previewable/preview_url/thumbnail_url, so SearchResultAttachment is an optional-field superset of both variants (the TimelineAttachment idiom) with only the 4 both-variant keys required. RichTextAttachment itself is untouched — its other emitters do guarantee those fields.

openapi diff is exactly the intended change: required [app_url,content,description,id,title,type,url] → [content,description], 26 properties added, 0 removed, one new schema, paths identical, no projection side effects.

Fixtures: spec/fixtures/search/results.json grew from 4 to 8 hits — one per special branch, including a float-spelled 1920.0 width, a chat-upload bespoke aggregate, color: null, and on_hold — with the coverage guard red-proved against the old schema (fails on exactly the five demotions + the unknown shape) and green after. Go/TS/Ruby/Python search tests each assert an attachment-branch hit end-to-end; the Go wrapper carries the 26 fields under the wrapper-drift gate (92 pairs, 1245 fields).

person_minimal's guaranteed keys become @required (#659)

people/_person_minimal.json.jbuilder unconditionally emits id, name, avatar_url (avatar_url is a route helper, never nil); of its three consuming shapes, UpcomingSchedulePerson already marked all three @required. MyAssignmentAssignee and OutOfOfficePerson now match. No projection patch needed — proven by build+diff: exactly the two schemas' required arrays change and nothing else.

Generated flips: Go *string → string; Swift var String? → let String (decode now throws on a missing key — noted as class B in MIGRATING); TS name?: → name:; Python NotRequired[str] → str; Ruby required_fields. Kotlin is a no-op for both commits (no typed Kotlin model exists for these shapes; services return JsonElement).

MIGRATING

New Unreleased section with per-SDK was/now tables for both changes, counts derived per the document's stated-derivation rule.

Verification

Full serial make check green at HEAD ("All checks passed"), including go/kt/py drift, check-go-optional-pointers, check-kotlin-optional-arrays-and-scalars, fixture coverage, wrapper drift, and doc-constants. Per-suite: Go 0 failures, Ruby 1412 runs/0 failures, TS 1485 passed, Python 1215 passed, Swift 375 passed.

Deliberately out of scope (declared in the commit message): the attachment branch's grandparent parent-id/title quirk (semantic, not structural) and chat lines' dead subscription_url/position. Follow-up to be filed on close: a six-runner Search conformance fixture (no runner has a mock Search fixture today; Swift doesn't dispatch Search at all).


Summary by cubic

Models the four special-cased search result branches and makes person_minimal’s three keys required. Previously SearchResult required id/title/type/url/app_url; attachment hits omitted them and the Go wrapper fabricated zeros. Now only content and description are required, and the Go wrapper uses omitempty to preserve key absence.

  • Adds optional fields for attachments, kanban lists, gauge needles, chat lines, and shared envelope; width/height accept float-spelled values. Top-level attachments is now a typed SearchResultAttachment that covers both rich-text and chat-upload shapes.
  • Requires avatar_url and name on MyAssignmentAssignee and OutOfOfficePerson, matching person_minimal. Adds per-SDK tests asserting all three keys on My Assignments reads; updates the out-of-office disabled stub to include avatar_url.
  • Retires the “attachments” gap in docs by marking it absorbed; the spec references SearchResult$attachments/SearchResultAttachment.

Migration

  • Do not assume id, title, type, url, or app_url exist on every SearchResult. Continue to expect content and description as present (nullable).
  • Treat attachments elements as SearchResultAttachment; do not assume the rich‑text attachment shape.
  • Update callers to require avatar_url and name on MyAssignmentAssignee and OutOfOfficePerson (e.g., non-optional in Swift/TS, non-pointer in Go, required in Python/Ruby).