Repository navigation
feat(lists): saved list views — shared + personal views, forking, defaults (G40) #81
Description
Surfaced by the 2026-09-14 coverage re-measure. No previous issue or planning-doc revision mentions this feature at all — it was missed by every prior parity sweep.
The gap
Five live operations the client does not build. All are x-auth-type: sync-token (Bearer-reachable) and x-subscription-tier: free, so this is buildable today and gates on nothing.
| Operation | What the spec says |
|---|---|
GET /api/lists/{id}/views |
"Every shared view on the list, plus this user's own personal views." |
POST /api/lists/{id}/views |
Creates a saved view — {name, scope, config, isDefault} |
POST /api/lists/{id}/views/{viewId} |
"Forks a shared view into a personal copy owned by the caller — the escape hatch." |
PUT /api/lists/{id}/views/{viewId} |
Updates a view's name, config or default flag |
DELETE /api/lists/{id}/views/{viewId} |
Removes a view |
Why it matters
The shape implies a real collaboration model, not a cosmetic preference:
- shared views belong to the list and everyone with access sees them
- personal views belong to the caller
- fork exists as a deliberate "escape hatch" — you can take someone else's shared view and make it yours rather than being stuck with how the list owner arranged it
isDefaultis per-user, so two people can open the same list and correctly see different default arrangements
macOS currently has one hard-coded arrangement per list. On a shared list the owner's choices are simply not visible to collaborators.
⚠️ Probe before modelling — config is a string
config is declared {"type": "string"}, not an object, so the schema says nothing about its grammar. It presumably encodes column order / visibility / sort / filter, but that is an inference.
Capture a real saved view from the web app before writing any decoder. This repo has a documented history of exactly this failure — G21 link metadata (nested metadata decoded to all-nil, so every timeline link rendered host-only) and G25 org members (OrganizationMemberDTO required userId where live rows key by id). Both had green tests against fabricated fixtures. Do not add a third.
Also worth confirming: whether scope is the shared/personal discriminator and what its token set is.
Sequencing
Additive — nothing currently shipped is wrong without it. Suggest after the account cluster (#44 → #47 → #46 → #45).
Acceptance
- Views listed, created, updated, deleted, and forked from macOS.
- Shared vs personal is visually distinct; the fork affordance is discoverable.
isDefaultis honoured per user on list open.configis modelled against a captured payload, with a contract test that fails on a renamed key rather than degrading tonil.- BDD quartet across Kit / Domain / App.
Recorded as G40 in work-consolidation.md §1f.
Activity
Adron commentedon Sep 16, 2026
Implemented in PR #105 (G40). Work continues on the PR.
All five routes, built against captured live payloads rather than the OpenAPI document — which mattered, because the spec is wrong in two places and both were caught:
createdAt/updatedAtare marked required in theListViewschema and absent from every live response. A strict decoder mirroring the spec would fail on every row.config.filters: [{"key":"read","op":"eq","value":false}]. That filter was sent live and dropped to[]. There is a test recording it, so nobody builds a fixture from that example.The eight live keys decode non-optionally, so a renamed key fails loudly rather than degrading to
nil— this issue's explicit acceptance criterion, and the G21/G25 failure mode.It does not.
configis a server-normalized whitelist of exactly four keys —mode,density,filters,search. Ten column/sort keys were sent and silently stripped without error. Worth correcting inwork-consolidation.md§1f when the doc track updates G40.The validation asymmetry is encoded in the types:
scopeis a closedpersonal|sharedenum because the server hard-fails an unknown token (400 scope must be "personal" or "shared"), whilemodeanddensitycarry.unknown(String)because the server silently defaults — so an unrecognised value is a real state that must not be overwritten on save.Still unconfirmed
The
filterselement grammar. The recon account's only list has an empty schema, so every filter references an unknown column key and is dropped — grammar failure and column-not-found are indistinguishable. Preserved opaquely and commented as unconfirmed rather than guessed. Re-probe once a schema'd list exists.Flagged by the implementer, worth a look in review
Shared-view writes by a non-owner are untested — the read-only branch hides shared create/rename/delete on a watcher share on the assumption they 403. If they are actually allowed, the gate is too tight (not broken).
Merge pull request #105 from CompositeCode/feat/saved-list-views-g40