Skip to content

view-authoring-live: the documented view-container authoring path is inert at runtime — the container is stored but never served #7736

Closed

Description

@huangyiirene

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:{…}} → 200 state:'draft'
  • POST /api/v1/meta/view/<name>/publish → 200
  • GET /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.ts iterates ARTIFACT_FIELD_TO_TYPE over the built objectstack.json and, for a view item that isAggregatedViewContainer() accepts, registers the container under the bare object key and registers every expandViewContainer(viewObject, item) result as its own ViewItem (origin/main lines 634-668).
  • The runtime write door stores the container verbatim. packages/metadata-protocol/src/protocol.ts imports isAggregatedViewContainer (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 calls expandViewContainer — that symbol has zero references anywhere in packages/metadata-protocol.
  • MetadataManager.getViewsByObject() (packages/metadata/src/metadata-manager.ts:1432-1444) filters v.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 a viewKind) — 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

  1. Boot the showcase with writable runtime packages.
  2. PUT /api/v1/meta/view/<name>?mode=draft&package=<pkg> with a ViewSchema container: {object: '<obj>', list: {…}, form: {…}} → 200 state:'draft'.
  3. POST /api/v1/meta/view/<name>/publish → 200.
  4. GET /api/v1/meta/view?object=<obj> → {"items":[]}, while GET /api/v1/meta/view/<name> returns the full body.
  5. Restart the server and repeat step 4 — still {"items":[]}.
  6. 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

  1. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    Deferred one round by the domain:metadata seat — 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 reach expandViewContainer, and that door is in protocol.ts, which is held right now by #7774 (dispatched 04:03Z, metaItemKey discriminator). 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.ts as of 04:0xZ: #7774 (in flight) → #7491 → #7736 → #6504 → #4716.

    What is already established, so the next dispatch does not re-derive it:


    Generated by Claude Code

  2. self-assigned this
    on Aug 12, 2026
  3. huangyiirene commented on Aug 13, 2026

    @huangyiirene
    CollaboratorAuthor

    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.

    ⚠️ Disclosure: I merged PR #8257 against the dev's stated instruction

    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.

    Probe Result
    Write a defineView container via saveMetaItem accepted
    Stored body verbatim — list, listViews, name; no object, no viewKind
    getMetaItem by name present, badged _diagnostics: {valid: true}
    getMetaItems({type:'view'}) 0
    Switcher predicate (viewKind && object===) 0
    Control — flat ViewItem written the same way 1 served, 1 switcher match ✅
    Well-formedness — expand the stored body 2 items, both would match the switcher

    ⭐ 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.ts boot loop, metadata/plugin.ts artifact/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 expandViewContainer hard-codes scope:'package' while sys_view_definition forbids that value at runtime. Reading the card corrected it — the repro is a writable runtime package, where package scope is right and sys_view_definition is 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:

    1. Loosen the getMetaItems container 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.
    2. Fix at a read exit. There are two independent object-bound readers: REST /meta/view?object= goes through getMetaItems, but getViewsByObject() reads MetadataManager.list. Fixing either one fixes the literal repro and leaves its sibling empty — exactly Object-extension overlay fields are missing from GET /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)

    Case Fix on Fix off Attribution
    Serves expanded ViewItems ✓ 🔴 headline
    Serves the stored container, not a default ✓ 🔴 anti-substitution
    No-container object stays empty ✓ 🔴¹ anti-vacuity
    Container never enumerated (ADR-0017) ✓ ✓ filter untouched
    Independent ViewItem unchanged ✓ ✓ not "serve whatever exists"
    Container stored byte-identical ✓ ✓ nothing extra persisted

    ¹ 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-debt exit 0 on a built closure, no ledger entry raised, nothing --lowered · check:nul-bytes OK · check:empty-changeset OK · 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 at packages/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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions