Skip to content

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

Draft
zhuangjianguo wants to merge 1 commit into
mainfrom
claude/issue-13407-runtime-view-container-hydration
Draft

fix(metadata-protocol): serve runtime-authored view containers to org/environment-scoped reads#13929
zhuangjianguo wants to merge 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.

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

Labels

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

2 participants