Skip to content

fix(metadata-protocol): serve runtime-authored view containers to org/environment-scoped reads - #13929

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13407-runtime-view-container-hydration
Sep 1, 2026
Merged

fix(metadata-protocol): serve runtime-authored view containers to org/environment-scoped reads#13929
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13407-runtime-view-container-hydration

Conversation

@zhuangjianguo

@zhuangjianguo zhuangjianguo commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes #13407

LEAD WITH THIS (as directed) — not the symptom

The card is right that this is not an org-scoping defect at the door: org-scoped
views authored as expanded ViewItems are correctly returned to their own org and
hidden cross-org. The discriminator is container-vs-expanded, org-independent — and
this PR's own new org-isolation pin re-confirms that is still true after the fix.

A2.1 verdict — FIRST, as directed

Falsified, precisely: the canonical shape {list:{type,data:{provider:'object', object:'note'},columns}} carries no top-level viewKind, so isAggregatedViewContainer
classifying it as a container is correct, not a write-path defect — that is exactly
what an unnamed single-view container looks like. There is no second write-path defect.

What actually explains "both shapes landed active, neither was returned" is not
shape at all — it is two isolation gates on the hydration mechanism itself, confirmed
below.

Clause ② determination (judged from the actual diff)

Path limb — NO. The 3 changed files: packages/metadata-protocol/src/protocol.ts,
packages/metadata-protocol/src/view-container-runtime-expansion.test.ts,
.changeset/view-container-runtime-org-environment-scope-closure.md. None under
packages/spec/src/**.

Content limb — FIRES. getMetaItems is a documented service method
(content/docs/kernel/services-checklist.mdx) and the backing implementation of the
published REST route GET /meta/view?object= (rest-server.ts). For an org-scoped or
environment-scoped request, it now returns rows it previously silently omitted — a
runtime-authored aggregated view container's expanded ViewItems. That is a response-shape
change on a published read door, independent of whether the omission was itself a bug:
a consumer enumerating that route sees more rows than before.

The new private expandRuntimeViewContainer member does reach the emitted .d.ts
(confirmed: grep -c expandRuntimeViewContainer dist/index.d.ts → 7) but does not
appear on any of the three public interfaces ObjectStackProtocolImplementation
implements (DataProtocol/MetadataProtocol/PackageProtocol from @objectstack/spec/api)
— structurally unreachable through a typed reference, and the downstream-consumer
typecheck (101/101 packages) confirms no consumer's compilation is affected. This half of
the content limb reads as benign; the firing reason is the response-content widening
above, not the type surface.

Docs cross-check (docs-drift-check's 10 named pages + the 3 release-owned pages).
Read in full: content/docs/concepts/metadata-lifecycle.mdx (the getMetaItems overlay-
cache passage describes caching/invalidation only — unaffected, unchanged) and
content/docs/ui/views.mdx (states the loader "expands the container into independently
addressable OBJECT.KEY view items" — a general claim my fix makes more true, not
less; the flat-vs-container authoring warning nearby is about defineStack/defineView
authoring shape, unrelated to org/environment read scope). Keyword-grepped (aggregated view|container.*drop|drop.*container|getMetaItems|only expanded|viewKind) with zero hits
on the remaining named pages: deployment/environment-variables.mdx (one hit — the
overlay-cache TTL passage, same unaffected mechanism as above), kernel/services- checklist.mdx (one hit — a table row listing getMetaItems, no behavioural claim),
ui/forms.mdx, ui/actions.mdx, ui/public-data-collection.mdx,
protocol/objectui/actions.mdx, protocol/objectui/layout-dsl.mdx, api/client-sdk.mdx
(the last one's dot-qualified sub-resource example, crm_lead.pipeline, is the expanded-
item naming convention my fix now honours universally rather than only for unscoped/env-
wide rows). No page states or implies that a view container is dropped from the listing
or that org/environment-scoped containers are never expanded — the omission this PR fixes
was never a documented behaviour, so no page is falsified.
Also checked (release-owned,
not editable here): releases/v14.mdx (no hit), releases/v16.mdx (one hit — defineView
non-empty-container enforcement, an authoring-time validation concern, unrelated),
releases/v17.mdx (no hit). None falsified.

Label: needs:contract-review attached below, per the content limb firing.

#7163 / #7736 scope-boundary statement (deliverable, not background)

Shape (a), as directed — what's actually fixed

  1. Object-derivation fix (the literal ask): hydrateExpandedViewItems's fallback
    chain (list.data.object → form.data.object → name) never read the container's own
    top-level object field — the field ViewSchema declares specifically for this
    purpose ("how a stack-level views: [...] entry says which object its views belong
    to; read by getViewsByObject() / GET /meta/view?object="). Now checked first.
  2. Overruled upward (clause ②'s "overrule me upward only", and view-authoring-live: the documented view-container authoring path is inert at runtime — the container is stored but never served #7736's own
    documented intent that this hydration serve every reader): the derivation fix ALONE
    does not make the card's own repro pass, because the function it lives in is never
    reached for an org-scoped or environment-scoped write (above). Widening either
    isolation gate was rejected as unsafe to do unilaterally — so instead
    getMetaItems now ALSO expands any aggregated container it reads, inline, into
    this request's own response only
    (expandRuntimeViewContainer, shared with the
    registry path) — never into the SchemaRegistry. This is safe for isolation because
    it operates exclusively on overlays, the rows getMetaItems already read for
    this request's own org/environment scope (the existing queryByOrg merge); nothing
    is written anywhere another request or kernel could see it from — see the new
    registry-emptiness assertion in the tests below, which checks this directly.

getMetaItems's canonical-shape filter (the container drop, protocol.ts — STOP
condition 2) is untouched: the raw container is still dropped from every listing; only
the expanded items are now also present, restoring the same invariant #7736 already
established for the narrower case.

What was NOT touched, and why

  • The two isolation gates above (environmentId, organizationId) — deliberate,
    correctly-reasoned boundaries (ADR-0005; the SchemaRegistry is shared by every
    request a kernel serves). Widening them was the tempting shortcut and was rejected;
    the inline, registry-free expansion achieves the same result without touching them.
  • metadata-manager.ts's getViewsByObject() (the card's second cited exit) — reads a
    different backing store (MetadataManager's own loader-based registry, not
    sys_metadata via getMetaItems). The card's own repro and pin
    (GET /meta/view?object=) go through getMetaItems/REST only; fixing the sibling
    exit is a materially larger, separately-scoped change (own tests, own package
    discipline) and is filed as a follow-up, not bundled here.
  • packages/metadata/src/plugin.ts (the artifact/HMR loader) carries the identical
    object-derivation bug (never reads container.object either) — filed as a follow-up
    (same defect class, but a different file/verification surface, so it does not qualify
    for the bounded in-place-fix exemption).
  • rest-server.ts's viewKind filter — untouched, per the card's own non-negotiable.
  • getMetaItems's container-enumeration drop — untouched (STOP condition 2 preserved).

Path collision (STOP condition 4) — measured, not assumed

PR #13870's protocol.ts diff lands at 1370–1454 (new MalformedVersionTokenError /
assertVersionTokenNotMalformed) and 10126–10290 (assertVersionOf/
assertVersionMatch OCC methods) — confirmed via pull_request_read get_files. My
edits are at hydrateOverlayIntoRegistry/hydrateExpandedViewItems
(~12830–12995) and the getMetaItems overlay-merge section (~6495–6530): thousands of
lines from either region, no overlap.

Anchors re-located by quoted source (drift measured)

Card cites Actual (this branch) Drift
rest-server.ts:4404 (viewKind filter) rest-server.ts:4811 ~407 lines
metadata-manager.ts:1592 (getViewsByObject) metadata-manager.ts:1583 ~9 lines
protocol.ts:6358 (container drop in getMetaItems) protocol.ts:6731 ~373 lines
protocol.ts:12483 (hydrateExpandedViewItems) protocol.ts:12919 (pre-fix) ~436 lines

No file-identity drift; no ~4,600-line drift found in any of the four cited files on
this branch (that measurement belongs to a different lane's file, per the dispatch
order's own wording).

Tests

All commands re-run on this PR's HEAD (7f63e54ced) after merging origin/main
(4642f4c64c) into the branch:

  • New behavioral pin + positive control (view-container-runtime-expansion.test.ts,
    new #13407 org-scoped and environment-scoped runtime containers are served describe
    block, 8 new cases alongside the 8 pre-existing #7736 ones): the card's own repro
    (org-scoped runtime container → getMetaItems), a positive control (an independent
    ViewItem for the same object stays served alongside), org isolation (visible to its own
    org, hidden from a different one), a registry-emptiness assertion (the isolation-safe
    path never writes the shared SchemaRegistry), the environment-scoped-kernel case, the
    container.object-derivation case, and 2 anti-vacuity cases.
  • Ablation (mutation on disk proved both ways, restore proved byte-identical under a
    trap with absolute paths): protocol.ts replaced with the pre-fix (parent commit)
    blob — fix-marker greps 1→0 in both directions, old buggy chain 0→1, blob hash
    68d810af1b…03be73cf57…; rebuilt; exactly the 5 new cases that depend on the fix
    FAIL (derives the object binding…, the card's own org repro, the positive
    control, org isolation, the environment-scoped case — each expect([...]).toEqual([])
    or equivalent, and the positive-control case degrades to only the pre-existing
    independent item surviving, exactly as a fix that "returns everything" being absent
    predicts) while the pre-existing #7736 cases, the registry-emptiness case (true either
    way — the pre-fix mechanism never touched the registry for org rows either) and the 2
    anti-vacuity cases stay green (11 passed / 5 failed of 16), confirming the ablation
    targets only the new mechanism and nothing wider. git checkout HEAD -- PATH
    restore verified byte-identical (git hash-object back to 68d810af1b…; git diff HEAD and git status --porcelain both empty) under a trap with REPO_ROOT absolute
    paths (EXIT INT TERM). Rebuilt and re-ran: 16/16 pass restored.
  • pnpm --filter @objectstack/metadata-protocol test (full package suite): 147
    passed / 2 skipped (149 files), 2053 passed / 10 skipped (2063 tests) — 0 failures, 0
    new skips (the 2 skipped files and 10 skipped tests are pre-existing).
  • turbo run typecheck --filter=@objectstack/metadata-protocol: 13/13 tasks, clean.
  • Downstream consumers (turbo run typecheck --filter='...@objectstack/metadata-protocol',
    the consumer-prefix direction): 101/101 tasks pass — no downstream package's typecheck
    regressed from the new (additive, still-private) expandRuntimeViewContainer member
    appearing in the emitted .d.ts.
  • packages/rest view/meta-read consumer subset (10 files identified by grep for
    meta/view|viewKind|getViewsByObject across packages/rest/src/*.test.ts — the REST
    route this card's repro exercises): 10/10 files, 137/137 tests pass.
  • Full workspace build (turbo run build --filter='./packages/*' --filter='./packages/*/*'):
    70/70 tasks pass.
  • ESLint, scoped to the 2 changed files (eslint --no-inline-config, --format json
    count confirms 2 files measured): 0 errors, 0 warnings. Proven-narrowing, not merely
    skipped: this repo's eslint.config.mjs "never enables type-aware linting (no
    parserOptions.project, no typed @typescript-eslint rules) for ANY file" (its own
    stated invariant), so no untouched file's judgment can depend on these two — a per-file
    scope is a complete measurement here, not a narrowed one.
  • Gate family (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack,
    re-derived on the fresh, non-stale tree after merging origin/main — 36 commands): all
    35 reachable commands pass. The 36th, node scripts/check-test-completeness.mjs (no
    args), is NOT MEASURED — it grades a saved turbo run test log path that only CI
    produces; its own text: "not a red… not a finding." The scheduled (not PR-gating)
    node scripts/pm/check-half-states.mjs live-repo sweep is likewise NOT MEASURED
    locally (network/live-repo bound); the PR-blocking form, pnpm check:pm-half-states
    (--self-test only), passed (1826 cases).

Generated by Claude Code

…/environment-scoped reads (#13407)

Fixes #13407

hydrateExpandedViewItems's object-derivation now checks the container's own
top-level `object` field first (ViewSchema.object), before falling back to
list.data.object -> form.data.object -> name.

getMetaItems now also expands an aggregated view container inline, into that
request's own response only -- never into the shared SchemaRegistry -- so an
org-scoped or environment-scoped runtime-authored container reaches its own
GET /meta/view?object= read, without touching either isolation gate
(applyRegistryWriteThrough's environmentId gate, hydrateOverlayIntoRegistry's
organizationId gate).
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 9 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via getBookTree (sdk, the bare tail of client method meta.getBookTree, bound to GET /api/v1/meta/book/:name/tree), meta.getBookTree (sdk, the route ledger binds it to GET /api/v1/meta/book/:name/tree, selected by route anchor /book/:name/tree))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), getMetaItems (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/deployment/environment-variables.mdx (via getMetaItems (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/services-checklist.mdx (via getMetaItems (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/protocol/objectui/actions.mdx (via /forms/:slug (route, bridged from symbol getMetaItems — its registrar handler names it))
  • content/docs/protocol/objectui/layout-dsl.mdx (via /forms/:slug (route, bridged from symbol getMetaItems — its registrar handler names it))
  • content/docs/ui/actions.mdx (via /forms/:slug (route, bridged from symbol getMetaItems — its registrar handler names it))
  • content/docs/ui/forms.mdx (via /forms/:slug (route, bridged from symbol getMetaItems — its registrar handler names it), /forms/:slug/lookup/:field (route, bridged from symbol getMetaItems — its registrar handler names it))
  • content/docs/ui/public-data-collection.mdx (via /forms/:slug (route, bridged from symbol getMetaItems — its registrar handler names it))
  • content/docs/ui/views.mdx (via /forms/:slug (route, bridged from symbol getMetaItems — its registrar handler names it))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via /book/:name/tree (route, bridged from symbol getMetaItems — its registrar handler names it))
  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1packageMentionDocs.

Which tree this was computed on

This run read content/docs from f3ed5e109a69499787191f36efeef62e6bf2e770 — the merge of head 7f63e54ced4d43681464e6c287de1a0655b543ee into base 55519d50363147da03df0af068fb82fcc6d1d8c1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f3ed5e109a69499787191f36efeef62e6bf2e770 && git checkout f3ed5e109a69499787191f36efeef62e6bf2e770
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55519d50363147da03df0af068fb82fcc6d1d8c1 7f63e54ced4d43681464e6c287de1a0655b543ee && git checkout -B drift-repro 55519d50363147da03df0af068fb82fcc6d1d8c1 && git merge --no-ff 7f63e54ced4d43681464e6c287de1a0655b543ee

node scripts/docs-audit/affected-docs.mjs --json 55519d50363147da03df0af068fb82fcc6d1d8c1

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 55519d50363147da03df0af068fb82fcc6d1d8c1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

os-sam commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Contract review (Clause ②) — PASS

Reviewed at head 7f63e54ced by the director seat (maintainer-summoned session session_015adLit3ZYASJiXwxKG78Wi), at tier in its own session — machine-read fuse: get_sessionlast_served_model equals CONTRACT_REVIEW_TIER; not the dispatching seat. Reviewed against the actual protocol.ts diff and the changeset, not the PR body.

VERDICT: PASS
CLAUSE-2-PATH: no
CLAUSE-2-CONTENT: yes
DECLARATION-HONEST: yes
ONE-LINE: The content-limb widening (org/environment-scoped `getMetaItems` reads now include a runtime-authored container's expanded ViewItems) is a defect repair restoring documented behaviour, achieved without moving either isolation gate — the expansion is per-request, registry-free, and idempotent against the pre-existing hydration path.
FINDINGS:
- Isolation verified at source, the crux of this review: the new inline pass operates only on `overlays` — rows the surrounding `getMetaItems` code already read for THIS request's own org/environment scope — and writes them only into the returned `items` array. The two isolation gates (`applyRegistryWriteThrough`'s `environmentId` guard; `hydrateOverlayIntoRegistry`'s org-row refusal, ADR-0005/[#6602]) are byte-untouched, and the shared SchemaRegistry is never written on this path (pinned by the suite's registry-emptiness assertion).
- The refactor is semantics-preserving: `hydrateExpandedViewItems` now delegates to the extracted pure `expandRuntimeViewContainer` — same expansion, same provenance carry, same artifact-protection merge, same `registerItem` sink. One expansion implementation, two sinks with different scope rules, each stated.
- The derivation fix consults the DECLARED field first (`ViewSchema.object` — the field documented as existing for exactly this read) and keeps the legacy three-deep fallback unchanged — declared=enforced restoration, not a new dialect.
- The inline upsert is by `name` (Map.set), matching the registry path's registerItem-by-name semantics, so the two paths cannot produce duplicates on an unscoped kernel where both run.
- The raw container stays dropped from every listing (the canonical-shape filter is untouched) — STOP condition 2 preserved; only expanded items appear.
- Changeset grade `patch` judged HONEST and accepted: no new key, shape, or vocabulary appears on the wire — rows previously omitted (in violation of the documented invariant #7736 established for the unscoped case) are now present, in the exact row shape the route has always served. This is an omission repair, not a surface addition; the `minor`-grade precedents on this lane all added new envelope members, which this does not.
- The private `expandRuntimeViewContainer` reaching the emitted `.d.ts` is structurally unreachable through the three published protocol interfaces (the PR measured 101/101 downstream typechecks) — benign.

Carrier action: needs:contract-review cleared on this PR (card #13407 never carried it). Landing is the dispatching seat's: pre-landing checks → ready → queue, with every check green at the head.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 1, 2026 01:45
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit d101943 Sep 1, 2026
43 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13407-runtime-view-container-hydration branch September 1, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rest/meta: getViewsByObject / GET /meta/view?object= omits a runtime-authored view CONTAINER — #7163/#7736 expansion fix does not cover this path

3 participants