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

@zhuangjianguozhuangjianguo 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 citesActual (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-samClaude

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

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@zhuangjianguo@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(metadata-protocol): serve runtime-authored view containers to org/environment-scoped reads by zhuangjianguo · Pull Request #13929 · objectstack-ai/objectstack · GitHub
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

@zhuangjianguozhuangjianguo 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 citesActual (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-samClaude

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

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@zhuangjianguo@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(metadata-protocol): serve runtime-authored view containers to org/environment-scoped reads by zhuangjianguo · Pull Request #13929 · objectstack-ai/objectstack · GitHub
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

@zhuangjianguozhuangjianguo 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 citesActual (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-samClaude

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

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@zhuangjianguo@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(metadata-protocol): serve runtime-authored view containers to org/environment-scoped reads by zhuangjianguo · Pull Request #13929 · objectstack-ai/objectstack · GitHub
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

@zhuangjianguozhuangjianguo 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 citesActual (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-samClaude

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

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@zhuangjianguo@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(metadata-protocol): serve runtime-authored view containers to org/environment-scoped reads by zhuangjianguo · Pull Request #13929 · objectstack-ai/objectstack · GitHub
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

@zhuangjianguozhuangjianguo 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 citesActual (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-samClaude

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

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@zhuangjianguo@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(metadata-protocol): serve runtime-authored view containers to org/environment-scoped reads by zhuangjianguo · Pull Request #13929 · objectstack-ai/objectstack · GitHub
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

@zhuangjianguozhuangjianguo 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 citesActual (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-samClaude

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

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@zhuangjianguo@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(metadata-protocol): serve runtime-authored view containers to org/environment-scoped reads by zhuangjianguo · Pull Request #13929 · objectstack-ai/objectstack · GitHub
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

@zhuangjianguozhuangjianguo 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 citesActual (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-samClaude

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

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@zhuangjianguo@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(metadata-protocol): serve runtime-authored view containers to org/environment-scoped reads by zhuangjianguo · Pull Request #13929 · objectstack-ai/objectstack · GitHub
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

@zhuangjianguozhuangjianguo 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 citesActual (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-samClaude

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

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@zhuangjianguo@os-sam@claude