fix(metadata-protocol): serve runtime-authored view containers to org/environment-scoped reads - #13929
Conversation
…/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).
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
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-levelviewKind, soisAggregatedViewContainerclassifying 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 notshape 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 underpackages/spec/src/**.Content limb — FIRES.
getMetaItemsis a documented service method(
content/docs/kernel/services-checklist.mdx) and the backing implementation of thepublished REST route
GET /meta/view?object=(rest-server.ts). For an org-scoped orenvironment-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
expandRuntimeViewContainermember does reach the emitted.d.ts(confirmed:
grep -c expandRuntimeViewContainer dist/index.d.ts→ 7) but does notappear on any of the three public interfaces
ObjectStackProtocolImplementationimplements (
DataProtocol/MetadataProtocol/PackageProtocolfrom@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(thegetMetaItemsoverlay-cache passage describes caching/invalidation only — unaffected, unchanged) and
content/docs/ui/views.mdx(states the loader "expands the container into independentlyaddressable OBJECT.KEY view items" — a general claim my fix makes more true, not
less; the flat-vs-container authoring warning nearby is about
defineStack/defineViewauthoring shape, unrelated to org/environment read scope). Keyword-grepped (
aggregated view|container.*drop|drop.*container|getMetaItems|only expanded|viewKind) with zero hitson the remaining named pages:
deployment/environment-variables.mdx(one hit — theoverlay-cache TTL passage, same unaffected mechanism as above),
kernel/services- checklist.mdx(one hit — a table row listinggetMetaItems, 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 —defineViewnon-empty-container enforcement, an authoring-time validation concern, unrelated),
releases/v17.mdx(no hit). None falsified.Label:
needs:contract-reviewattached below, per the content limb firing.#7163 / #7736 scope-boundary statement (deliverable, not background)
[finding] the nested-plugin seam does not expand an aggregated
viewscontainer — a nested plugin's per-view items never reach the registry, sogetViewsByObject()/GET /meta/view?object=answer with the container alone #7163 scoped to boot-time, code-authored containers (engine.ts'smanifest/nested-plugin loop,
plugin.ts's artifact/HMR loader) — always local to theserving kernel's own registry, no environment/org gating question ever arises there.
view-authoring-live: the documented view-container authoring path is inert at runtime — the container is stored but never served #7736 scoped
hydrateExpandedViewItems's registry-hydration to exactly the ONEcombination its own pin (
view-container-runtime-expansion.test.ts) exercises: anunscoped protocol instance (
environmentId === undefined) writing an env-widerow (
organizationIdunset). That combination is not incidental — it is enforced bytwo gates, both correctly load-bearing for isolation, not oversights:
applyRegistryWriteThrough:if (this.environmentId !== undefined) return;— neverwrite-through hydrates on a real per-environment kernel (
assembleMetadataProtocol'sown comment: "per-project (cloud) kernels source metadata from the control plane").
hydrateOverlayIntoRegistry:if (options.organizationId !== null && options.organizationId !== undefined) return false;— never grafts an org-scopedrow into a registry every org sharing that kernel reads (ADR-0005 per-org isolation,
[#6602]).This card's own repro is "a signed-in user with an active org" — i.e. exactly the
case view-authoring-live: the documented view-container authoring path is inert at runtime — the container is stored but never served #7736's fix boundary excludes, regardless of environmentId. Empirically
confirmed against a stub protocol (see Tests): an env-wide, unscoped write expands
correctly (
#7736's existing pin, unchanged); the identical write withorganizationIdset expands to nothing, on the very same unscoped kernel. A separate probe against
an
environmentId-scoped protocol instance shows the identical failure for anywrite, org-scoped or not. That is the "fourth occurrence" triage warned the unnamed
scope boundary would invite — now named.
Shape (a), as directed — what's actually fixed
hydrateExpandedViewItems's fallbackchain (
list.data.object → form.data.object → name) never read the container's owntop-level
objectfield — the fieldViewSchemadeclares specifically for thispurpose ("how a stack-level
views: [...]entry says which object its views belongto; read by
getViewsByObject()/GET /meta/view?object="). Now checked first.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
getMetaItemsnow ALSO expands any aggregated container it reads, inline, intothis request's own response only (
expandRuntimeViewContainer, shared with theregistry path) — never into the SchemaRegistry. This is safe for isolation because
it operates exclusively on
overlays, the rowsgetMetaItemsalready read forthis request's own org/environment scope (the existing
queryByOrgmerge); nothingis 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— STOPcondition 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
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'sgetViewsByObject()(the card's second cited exit) — reads adifferent backing store (
MetadataManager's own loader-based registry, notsys_metadataviagetMetaItems). The card's own repro and pin(
GET /meta/view?object=) go throughgetMetaItems/REST only; fixing the siblingexit 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 identicalobject-derivation bug (never reads
container.objecteither) — 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'sviewKindfilter — 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.tsdiff lands at 1370–1454 (newMalformedVersionTokenError/assertVersionTokenNotMalformed) and 10126–10290 (assertVersionOf/assertVersionMatchOCC methods) — confirmed viapull_request_read get_files. Myedits are at
hydrateOverlayIntoRegistry/hydrateExpandedViewItems(~12830–12995) and the
getMetaItemsoverlay-merge section (~6495–6530): thousands oflines from either region, no overlap.
Anchors re-located by quoted source (drift measured)
rest-server.ts:4404(viewKind filter)rest-server.ts:4811metadata-manager.ts:1592(getViewsByObject)metadata-manager.ts:1583protocol.ts:6358(container drop ingetMetaItems)protocol.ts:6731protocol.ts:12483(hydrateExpandedViewItems)protocol.ts:12919(pre-fix)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 mergingorigin/main(
4642f4c64c) into the branch:view-container-runtime-expansion.test.ts,new
#13407 org-scoped and environment-scoped runtime containers are serveddescribeblock, 8 new cases alongside the 8 pre-existing
#7736ones): the card's own repro(org-scoped runtime container →
getMetaItems), a positive control (an independentViewItem 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.trap with absolute paths):
protocol.tsreplaced with the pre-fix (parent commit)blob — fix-marker greps
1→0in both directions, old buggy chain0→1, blob hash68d810af1b…→03be73cf57…; rebuilt; exactly the 5 new cases that depend on the fixFAIL (
derives the object binding…, the card's own org repro, the positivecontrol, 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
#7736cases, the registry-emptiness case (true eitherway — 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 -- PATHrestore verified byte-identical (
git hash-objectback to68d810af1b…;git diff HEADandgit status --porcelainboth empty) under a trap withREPO_ROOTabsolutepaths (
EXIT INT TERM). Rebuilt and re-ran: 16/16 pass restored.pnpm --filter @objectstack/metadata-protocol test(full package suite): 147passed / 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.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)
expandRuntimeViewContainermemberappearing in the emitted
.d.ts.packages/restview/meta-read consumer subset (10 files identified by grep formeta/view|viewKind|getViewsByObjectacrosspackages/rest/src/*.test.ts— the RESTroute this card's repro exercises): 10/10 files, 137/137 tests pass.
turbo run build --filter='./packages/*' --filter='./packages/*/*'):70/70 tasks pass.
eslint --no-inline-config,--format jsoncount confirms 2 files measured): 0 errors, 0 warnings. Proven-narrowing, not merely
skipped: this repo's
eslint.config.mjs"never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file" (its ownstated 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.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack,re-derived on the fresh, non-stale tree after merging
origin/main— 36 commands): all35 reachable commands pass. The 36th,
node scripts/check-test-completeness.mjs(noargs), is NOT MEASURED — it grades a saved
turbo run testlog path that only CIproduces; its own text: "not a red… not a finding." The scheduled (not PR-gating)
node scripts/pm/check-half-states.mjslive-repo sweep is likewise NOT MEASUREDlocally (network/live-repo bound); the PR-blocking form,
pnpm check:pm-half-states(
--self-testonly), passed (1826 cases).Generated by Claude Code