You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(spec)!: the view surface closes — the last batch of #4001 (#4534)
Views are the surface an author iterates on visually, which is why a dropped key
hides here: the view still renders, just not the way it was described.
`FormFieldBase` / `FormSection` / `FormButtonConfig` closed years ago under
ADR-0089 D3a; the other forty-odd shapes kept the posture those three were
rescued from.
`defineView`'s guard was another one-door workaround — its own comment says
"`ViewSchema` strips unknown top-level keys, so a flat list view would parse to
an empty container". Fifth bespoke guard this campaign has found built around
silent stripping, and like all of them it covered exactly one door. The
rejection now lives in the parse, reaches both, and carries the wrap
instruction. The guard stays for what strict cannot see: `defineView({})`.
Three shapes deliberately left open, each with its reason in the file rather
than a silent skip: `FormSectionSchema` (already closed with its own error map
and a transform), `UserFiltersSchema` (deliberately strips page-only keys, with
a test asserting it — changing that needs its own verification), and the
flattened Studio overlay in `ViewMetadataSchema`, which must stay open.
That last one is the `.extend()` trap the ledger warned about: closing
`ListViewSchema`/`FormViewSchema` silently closed the overlay too, turning a
shape the platform itself writes into a 422. Both `.strip()` back. It was caught
by a test whose name is the whole contract — someone wrote "preserves auxiliary
Studio round-trip keys without a strict-mode 422" before this campaign existed.
The container's `name`, `label` and `object` were tombstoned in the first draft
and should not have been: the platform writes all three. Fourth false guidance
claim in three batches. What fixed it was not more care but a different method —
scan every real payload of the shape in the repo and keep only the entries no
real payload contradicts. Six of nine survived. A tombstone asserts "nothing
legitimately writes this key", which is an empirical claim, and the codebase can
be asked.
`view` is the end state, not the last item of debt: its registered schema is a
union of three runtime shapes and a union is only as closed as its most open
member — the Studio overlay, a wire shape wearing the same type name. So the
campaign finishes at 24 of 25 closed with the 25th a documented permanent
exception, recorded beside the reverse pin so nobody closes it by force.
Verified: full monorepo suite 132/132 tasks green, spec 7241 tests, tsc clean,
8 generated artifacts current, all 10 spec gates green.
Claude-Session: https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY
Co-authored-by: Claude <noreply@anthropic.com>
The view surface closes — the container, both view kinds, and the ~28 config shapes under them. This is the last batch of #4001.
6
+
7
+
Views are the surface an author iterates on visually, which is exactly why a dropped key hides here: the view still renders, just not the way it was described. `FormFieldBaseSchema`, `FormSectionSchema` and `FormButtonConfigSchema` were closed years ago under ADR-0089 D3a; the other forty-odd shapes in the file kept the posture those three were rescued from.
8
+
9
+
**`defineView`'s guard was another one-door workaround.** It rejects a container that defines no views, and its comment says why: "`ViewSchema` strips unknown top-level keys, so a *flat* list view would parse to an empty container". That is the fifth bespoke guard this campaign has found built around silent stripping, and like all of them it covered exactly one door — `defineView`. Through the metadata door (Studio, the API, an agent) a flat view produced an empty container in silence. The rejection now lives in the parse, so it reaches both, and carries the wrap instruction rather than only the symptom. The guard stays for the case strict cannot see: `defineView({})`, which has no unknown keys and still registers nothing.
10
+
11
+
**The container carries its own identity and object binding, and the first draft tombstoned all three.**`name`, `label` and `object` were in that guidance list, telling authors they "belong to a single VIEW, not to the container" — which rejected shapes the platform itself writes: `saveMetaItem` sends the name, artifact-shipped containers (`service-ai/ai_traces`) carry it, the validation sweep injects it, and a stack-level `views: [...]` entry needs `object` to say which object its views belong to (`getViewsByObject()` reads that binding). All three are now declared — `object` as live with its consumer cited, `name`/`label` as dead *body* keys with the row column live, exactly as `translation` needed in batch 5.
12
+
13
+
Caught by the full monorepo suite — `@objectstack/objectql`, then `@objectstack/cli` — never by `packages/spec`. That is the fourth false guidance claim in three batches (`action.permissions`, `action.location`, `view.name`/`label`, `view.object`), and the fix that finally worked was not more care but a different method: **scan every real container payload in the repo and keep only the guidance entries no real payload contradicts.** Six of the nine survived. That check costs one command and should have run before the guidance, not after three CI failures.
14
+
15
+
The rule worth carrying: **a rejection's prose is behaviour, not documentation.** It tells an author what to do next, and a confidently wrong one is worse than none, because there is no reason to doubt it.
16
+
17
+
**Three shapes are deliberately left open, each with its reason in the file rather than a silent skip:**
18
+
19
+
-**`FormSectionSchema`** already closed under ADR-0089 D3a with `strictVisibilityError` and a `.transform()` that normalizes the `visibleWhen`/`visibility` pair. Converting means re-expressing that map as `guidance` and re-proving the transform — a refactor of working, tested behaviour, not a strictness change.
20
+
-**`UserFiltersSchema`** deliberately *strips*`tabs`/`showAllRecords`, which are page-only keys (ADR-0047), with a test asserting the drop. The likely right end state is a rejection saying "tabs are page-only" — but that is a behaviour change with a real consumer question behind it (something may pass a page-shaped block through relying on the strip to narrow it). The campaign's own rule is verify-then-enforce, and this batch did not verify it. Named as the one open shape in the file.
21
+
-**The flattened Studio overlay** in `ViewMetadataSchema` must stay open: it carries auxiliary round-trip keys (`isPinned`, `sortOrder`, …) that `saveMetaItem` persists verbatim.
22
+
23
+
**That last one is the trap the ledger warned about, arriving on schedule.**`.extend()` inherits strictness, so closing `ListViewSchema`/`FormViewSchema` for authoring silently made the overlay strict too — turning a shape *the platform itself writes* into a 422. Both members now `.strip()` back, with a comment saying the `.strip()` is load-bearing rather than leftover.
24
+
25
+
## `view` is the end state, not the last item of debt
26
+
27
+
The registered `view` schema stays `strip`, and it always will: it is a union of three runtime shapes and a union is only as closed as its most open member. That member is the Studio overlay above — a wire shape wearing the same type name.
28
+
29
+
So the campaign's final number is **24 of 25 registered types closed, with the 25th a documented permanent exception**. That is recorded in `metadata-type-schemas.test.ts` beside the reverse pin, so nobody "finishes the job" by force. What closed is everything an author writes; what stayed open is the thing the ledger's classification rule exists to distinguish — arriving here as the campaign's answer rather than as an exception to it.
30
+
31
+
## Where the campaign ends up
32
+
33
+
-**Registered types closed: 24 of 25** (from 9 when this line started), the last one exempt with a stated reason.
34
+
-**The ADR-0010 undeclared-envelope debt list is empty**, from the eight the structural walk opened it with.
35
+
-**The unknown-key warning layer has one covered root left** — `view`, and only its open member. When a layer built to warn about strip-mode metadata has almost nothing left to warn about, that is the ratchet finishing.
36
+
37
+
Authoring impact: a key none of these shapes declares is now rejected instead of silently discarded — it was already being ignored, so no working view changes.
@@ -885,6 +885,9 @@ const result = AiAgentCapabilities.parse(data);
885
885
886
886
| Property | Type | Required | Description |
887
887
| :--- | :--- | :--- | :--- |
888
+
|**name**|`string`| optional | Item name — supplied by the metadata door; for an object-scoped container it is the object name. |
889
+
|**label**|`string`| optional | Human-readable label shown in metadata lists. |
890
+
|**object**|`string`| optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. |
|**name**|`string`| optional | Item name — supplied by the metadata door; for an object-scoped container it is the object name. |
667
+
|**label**|`string`| optional | Human-readable label shown in metadata lists. |
668
+
|**object**|`string`| optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. |
0 commit comments