Repository navigation
view-authoring-live: the documented view-container authoring path is inert at runtime — the container is stored but never served #7736
Description
Symptom
Authoring a view through the runtime metadata door with the documented ViewSchema container shape succeeds at every step and produces nothing a consumer can see.
PUT /api/v1/meta/view/<name>?mode=draft&package=<pkg>with{object, list:{…}, form:{…}}→ 200state:'draft'POST /api/v1/meta/view/<name>/publish→ 200GET /api/v1/meta/view?object=<obj>→{"items":[]}GET /api/v1/meta/view/<name>→ 200 with the full authored body
So the item is stored — it is just never served. The end-user list keeps rendering synthesized default columns. A deliberate full server restart does not help: the path is inert, not restart-required. Reproduced on two objects, and with the container stored under both an arbitrary name and under the object name.
Control that isolates it. Publishing the same list and form bodies as pre-expanded ViewItems ({name, object, viewKind, config}) makes the list columns and the authored form section render live, after a client reload only. Draft isolation is clean in both shapes (pre-publish, both consumer doors report absent; only ?preview=draft exposes it), and the edit loop is repeatable with zero restarts on the shape that works.
Root cause
Container expansion exists, but it is wired to exactly one path — the artifact/package boot load — and the runtime write door is not on it.
packages/metadata/src/plugin.tsiteratesARTIFACT_FIELD_TO_TYPEover the builtobjectstack.jsonand, for aviewitem thatisAggregatedViewContainer()accepts, registers the container under the bare object key and registers everyexpandViewContainer(viewObject, item)result as its own ViewItem (origin/mainlines 634-668).- The runtime write door stores the container verbatim.
packages/metadata-protocol/src/protocol.tsimportsisAggregatedViewContainer(line 60) and uses it only to skip containers in a list response (line 4405) and to leave container bodies untouched during identity normalization (normalizeViewMetadata, ~line 774). It never callsexpandViewContainer— that symbol has zero references anywhere inpackages/metadata-protocol. MetadataManager.getViewsByObject()(packages/metadata/src/metadata-manager.ts:1432-1444) filtersv.viewKind && v.object === object. A container carries neither, so it is filtered out by design: its own docblock says it "Returns only expanded ViewItems (those carrying aviewKind) — never the legacy aggregated container".
The three facts compose into the measured behaviour: the write door accepts and persists the container, the expansion that would give it viewKind never runs off the boot path, and the by-object consumer drops anything without viewKind.
Stale-premise check: re-verified on objectstack origin/main (00e9196). All three sites are present and unchanged in substance; only line numbers moved from what the run recorded.
Neighbour: #7163 (closed) is the same missing-expansion class on a different seam — the nested-plugin loader. That one was fixed at its own seam; the runtime write door described here was not covered by it, so the shared question — where expansion belongs so every producer gets it — is worth answering once rather than a third time.
Reproduction
- Boot the showcase with writable runtime packages.
PUT /api/v1/meta/view/<name>?mode=draft&package=<pkg>with a ViewSchema container:{object: '<obj>', list: {…}, form: {…}}→ 200state:'draft'.POST /api/v1/meta/view/<name>/publish→ 200.GET /api/v1/meta/view?object=<obj>→{"items":[]}, whileGET /api/v1/meta/view/<name>returns the full body.- Restart the server and repeat step 4 — still
{"items":[]}. - Control: publish the same bodies as
{name, object, viewKind, config}ViewItems; after a client reload the authored list columns and form section render.
Source
Extracted from the QA run #7695 (framework 92f26f7, console 09987b680).
Activity
huangyiirene commentedon Aug 12, 2026
Deferred one round by the
domain:metadataseat — hot-file serialization, not priority. Recording it so a later sweep does not read this card as simply unpicked. It is a strong card: symptom, three composing root-cause sites, a control that isolates the seam, and a stale-premise re-check already done.Why deferred: the repair lands in
packages/metadata-protocol/src/protocol.ts— the runtime write door has to reachexpandViewContainer, and that door is inprotocol.ts, which is held right now by #7774 (dispatched 04:03Z,metaItemKeydiscriminator). That file took 6 merges in a single day recently; two agents in it at once produce conflicts that cost more than the wait.Serial queue on
protocol.tsas of 04:0xZ: #7774 (in flight) → #7491 → #7736 → #6504 → #4716.What is already established, so the next dispatch does not re-derive it:
protocol.tsdrifts within hours. The named symbols areisAggregatedViewContainer,expandViewContainer,normalizeViewMetadata,MetadataManager.getViewsByObject.viewscontainer — a nested plugin's per-view items never reach the registry, sogetViewsByObject()/GET /meta/view?object=answer with the container alone #7163 (closed) was the same missing-expansion class at the nested-plugin loader seam and was fixed at that seam. This is the third time the question "where does container expansion belong so every producer gets it" comes up. ⇒ Whoever dispatches this should decide deliberately between fixing this seam (cheap, consistent with [finding] the nested-plugin seam does not expand an aggregatedviewscontainer — a nested plugin's per-view items never reach the registry, sogetViewsByObject()/GET /meta/view?object=answer with the container alone #7163's precedent) and moving expansion to a single choke point every producer passes (fixes the class) — and if the second, that is a shape decision that should be ruled or escalated, ⛔ not left to the dev.getViewsByObject's filter is by design — its docblock says it returns only expanded ViewItems and never the aggregated container. So the repair is to make expansion happen, ⛔ not to loosen that filter. A fix that makes the container itself pass the by-object filter would be treating the symptom.Generated by Claude Code
huangyiirene commentedon Aug 13, 2026
Dev report relayed by the PM (its container is 403 on the whole GitHub API — the second attempt returned the sharper "org admin must connect the Claude GitHub App"). It read this card in the web UI via
WebFetch, so its verdict is against the full body.Its report ends "Open the PR as draft; I did not enable auto-merge and did not flip ready." I never saw that — the dev could not open a PR, so I created #8257 myself from its pushed branch with a body I wrote, reviewed the diff, and merged it as
0e1f6679b4.This is now the second time in one shift (the same happened on #8258/#8037), which makes it a process gap rather than an accident: a 403-blocked dev puts its merge conditions in the PR body, and reconstructing that body from the branch loses them. ⇒ Recorded on the seat post: when opening a PR on a blocked dev's behalf, ask for its intended PR body first, and treat "draft" as the default until the dev says otherwise.
On the merits the merge stands — the diff was reviewed, CI was fully green, and the report below confirms the change is what I judged it to be. But the instruction existed and I overrode it unknowingly, so it goes on the record.
The verdict: wrong-source bug — ⛔ not an unbuilt capability
premise_still_valid: true, and every claim reproduced independently before the dev read the card.defineViewcontainer viasaveMetaItemlist,listViews,name; noobject, noviewKindgetMetaItemby name_diagnostics: {valid: true}getMetaItems({type:'view'})viewKind && object===)ViewItemwritten the same way⭐ That last row is what decides the triage. The stored container is well-formed — it expands cleanly into exactly the items the switcher wants. So this is neither a malformed write nor #7893's "read path never built": the read path exists and demonstrably works (the control proves it), and the expansion capability exists too, applied at two of three entry points (
objectql/engine.tsboot loop,metadata/plugin.tsartifact/HMR loader — both green). The runtime door is the third seam and skipped it. That is the #7163 shape this card itself named.⭐ It also reports correcting itself: it initially leaned toward a design exit, on the grounds that
expandViewContainerhard-codesscope:'package'whilesys_view_definitionforbids that value at runtime. Reading the card corrected it — the repro is a writable runtime package, where package scope is right andsys_view_definitionis the wrong store to reason about. Stating the abandoned hypothesis is worth as much as the conclusion.⭐ The trap it avoided — and this is the valuable part
Two tempting wrong fixes, both rejected with reasons:
getMetaItemscontainer filter. That filter drops containers from enumeration on a stated invariant — "the registrar expands it into independent ViewItems" — true for source-registered rows, false for runtime-written ones. Loosening it would surface the legacy wrapper to every list consumer while still showing the switcher nothing. Filter left untouched./meta/view?object=goes throughgetMetaItems, butgetViewsByObject()readsMetadataManager.list. Fixing either one fixes the literal repro and leaves its sibling empty — exactly Object-extension overlay fields are missing fromGET /meta/object/:name(present in the list route) — the overlay's fields can never be set through the UI #7556's shape.So the fix went to
hydrateOverlayIntoRegistry, documented in-repo as "the ONE choke point all three hydration callers (boot, read-side, write-through) already share." One expansion there serves every reader, survives restart (the card noted restart didn't help), and preserves read-your-writes. ⛔ Nothing extra is persisted — one byte-identical row, expansion derived on hydration, so an edited container leaves no stale rows.Reverse-verification (fix disabled at the call site)
ViewItemunchanged¹ red on its positive half (
crm_lead→ 2); its anti-vacuity half (crm_account→ empty) holds under both arms — which is what proves the fixtures distinguish authored-container from no-container.Gates, all post-merge and after a completed build (⛔ never concurrent with dogfood): metadata-protocol 76/1102 · metadata 31/603 · objectql 192/3400 · rest 103/1731 · runtime 145/2195 · dogfood 99 files/673 ·
check:type-check-debtexit 0 on a built closure, no ledger entry raised, nothing--lowered ·check:nul-bytesOK ·check:empty-changesetOK · eslint clean.Out-of-scope finding, now filed
#8285 — ADR-0017 §3.4 cites
packages/platform-objects/src/metadata/sys-view-definition.object.ts, which does not exist; the object is atpackages/metadata-core/src/objects/sys-view-definition.object.ts. Out of the dev's region, so reported not fixed; it cited no number in the commit rather than invent one.⭐ This card is the sixth instance of "a 200 that means nothing happened" — publish succeeded at every step and produced nothing a user could see. That family (#7893, #8018,
reassignOrphanedMetadata, #7823, #7736, #8284) is carried on the seat post as an open convention question for the maintainer.Generated by Claude Code
fix(metadata-protocol): expand a runtime-authored view container so i…
docs(qa): re-point eight checklist items the 17.6.0 release-verificat…