Skip to content

fix(metadata-protocol): gate getMetaItem's overlay read on the metadata registry - #14908

Merged
hotlong merged 3 commits into
mainfrom
claude/issue-14770-getmetaitem-overlay-precedence
Sep 3, 2026
Merged

fix(metadata-protocol): gate getMetaItem's overlay read on the metadata registry#14908
hotlong merged 3 commits into
mainfrom
claude/issue-14770-getmetaitem-overlay-precedence

Conversation

@os-musk

@os-musk os-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes #14770

getMetaItem — the singular /meta read verb — now resolves its own read scope through organizationIdForMetaRead, the registry-derived predicate the REST /meta doors have applied since #9454 and that #14683 (PR #14767) moved inside the plural verb.

⚠️ Fix round — the four blocking findings from the contract review

Landed as a2ff587d4, then merged with origin/main as c5c3f062e. The gate itself is unchanged: the review verified it against the plural door line by line and confirmed the precedence-not-union reading holds.

# finding how it was cleared
B1 CI red: protocol-meta.test.ts:89-112 expected the org row on app both singular-verb fixtures re-spelled onto view; the vacuous sibling at :114-129 moved in the same edit and gained the assertion that makes it non-vacuous. Tests 95 passed (95), was 1 failed | 94 passed
B2 changeset said the plural gate shipped "in the previous release" now reads "gated by #14683 / PR #14767, which lands in the SAME release as this change"
B3 "the served document then vanishes at the next restart" is false for this door corrected in both places — PR body below and changeset. Nothing deletes the row; a restart drops the row's registry presence, which this door does not consult while a sys_metadata row answers
B4 the "what moves" enumeration was asserted complete and was not the fourth raw-org caller (runtime/src/domains/packages.ts:1239, applyPublishedSeeds) is named, in the body and in the code comment, together with the method that establishes the population

Advisory, also taken: A1 the write-side read/write-scope identity is qualified under the OS_METADATA_WRITABLE hatch; A3 the ADR-0029 D9 citation is now a quotation with its "Design only" status attached, not a paraphrase presented as a spelling; A4 the ADR-0005 decision block is credited with what it actually ranks. A2 ("six plugin-security sites" — actually five) lives on #14907, not in this PR, and was corrected there.

The defect, measured

At the merge base 84b8190ae, packages/metadata-protocol/src/protocol.ts:7348:

const orgId = request.organizationId;

spent by both overlay reads in the method, the second of which is at :7452:

const record = (orgId ? await findOverlay(orgId) : undefined)
    ?? await findOverlay(null);

⚠️ The card and its triage comment recorded this site at :7382-7383 and then :7453; on the merge base it is :7348 / :7452. Immaterial to the finding, recorded because the file keeps moving.

?? is precedence, not the plural verb's union. On getMetaItems the two queryByOrg reads are UNIONed, so an ungated organization can only ADD rows — the resurrection #14683 is about. Here it can SUBSTITUTE: on a type the registry declares allowOrgOverride: false, a pre-#6190 phantom org-scoped row — the kind loadMetaFromDb walks past and reportUnhydratableOrgScopedRows warns about — was served instead of the live env-wide document, to a caller that asked for the live one.

⚠️ Corrected (B3). An earlier draft of this paragraph ended "the served document then vanishes at the next restart". That is true of the sentence's source — the predicate's own docblock — and false of this door, for a measurable reason: nothing removes the phantom row. loadMetaFromDb filters organization_id: null (protocol.ts:20703-20705) and skips the rest; reportUnhydratableOrgScopedRows only console.warns (:21053); the sole engine.delete('sys_metadata', …) (:20552) is inside deleteMetaItem. So a restart did not clear the substitution — the same phantom was served again. What a restart drops is the row's registry presence, and this door does not consult the registry while a sys_metadata row answers.

The change

One line, plus the comment that carries the reasoning:

const orgId = organizationIdForMetaRead(request.type, request.organizationId);

Three properties, each deliberate:

⛔ Why precedence stays — the question Zone 2 C asked

The card's title names the read as "?? PRECEDENCE, not a union". That is the defect statement, and it is not a licence to convert the combinator. The repo settles this — but each citation only at its real scope, which the review corrected this section on:

  • ADR-0005's decision block does name this method: RUNTIME READ getMetaItem(type, name)1. sys_metadata WHERE (type, name, …, state='active') marked ← overlay (wins), then 2. SchemaRegistry / MetadataService ← artifact default. ⚠️ The pair it ranks is overlay-vs-artifact-default, not org-row-vs-env-wide-row. It settles that an overlay wins rather than merges; it does not by itself settle this method's inner ??. An earlier draft of this section overstated that.
  • What settles the inner one, first: ADR-0005 design principle 3 — "Customizations are full-JSON deltas, not field-level patches" — stores the entire item document per overlay row, so a layering has nothing to layer.
  • The patch model that would have given layering a meaning is gone. MetadataOverlaySchema was retired and deleted whole under ADR-0049 — ADR-0005 still names metadata-customization.zod.ts as a kept, future-phase model after #13135 retires it #13185, PR Retire the paper metadata-customization protocol with its full coupling set #13186, maintainer ruling of 2026-08-29 — and ADR-0126 §6 rules out the phase it was held for. Recorded as a correction inside principle 3 itself.
  • What settles it, second: organizationIdForMetaRead's own docblock quotes this expression(orgId ? findOverlay(orgId) : undefined) ?? findOverlay(null) — as the intended shape while defining Org-overridable metadata (view, dashboard) is accepted with a 200 state:'active' receipt but served by no read door #9454. Precedence is the behaviour that card was written against, not the thing it reported; what Org-overridable metadata (view, dashboard) is accepted with a 200 state:'active' receipt but served by no read door #9454 calls the bug is an org-less read resolving only the env-wide row.
  • ADR-0029 D9 corroborates, one type over. ⚠️ Quoted, not paraphrased: resolveObject "selects its base layer as overlay ?? owner instead of owner, then folds extend contributions". Its status line reads "Design only — nothing is implemented yet", so it is corroboration and not an authority this method rests on. An earlier draft rendered it as base = overlay ?? own and presented that paraphrase as the ADR's spelling.

⇒ The union question is answered against a union, with citations at their real weight, rather than left open. §2 of the new test pins it so a future reading of the title fails a test instead of landing.

The idempotence proof, discharged before the gate was written

The triage made the callee-side direction conditional on showing that moving the predicate inside changes no already-gating caller's scope. Let f(t, o) = organizationIdForMetaRead(t, o).

  • f(t, undefined) === undefined — the predicate returns early on an undefined organization, before it consults the registry flag. Every caller that names no organization reads exactly what it read before: rest/src/import-mapping.ts:66, rest/src/import-prepare.ts:416, rest-server.ts:8634, plugin-email's template read, service-analytics' draft probe, plugin-auth's metaReader. Test §3 sweeps this over the complete accepted-spelling population.
  • f(t, f(t, o)) === f(t, o)f answers o for an overridable type and undefined otherwise, so a second application cannot move it. Test §4 asserts it over the same derived population rather than a hand-listed sample.
  • The REST by-name door gates on the string this method folds to. It computes organizationIdForMetaRead(canonicalMetaUrlType(req.params.type), readCtx?.tenantId) at rest-server.ts:5660 and then passes type: req.params.type, the raw segment; this method folds that segment with the same function, so request.type at the gate is the identical string the door gated on. Its cached arm reaches here through getMetaItemCached, which folds first and forwards the same hoisted readOrganizationId — the same no-op one hop later.
  • organizationIdForMetaWrite has a character-identical body, so the three internal write-side pre-reads — saveMetaItem's destructive-change probe, publishMetaItem's seed-loader adapter, publishPackageDrafts' build probes — now read the partition their write lands in. Read scope and write scope cannot disagree, which is the property Org-overridable metadata (view, dashboard) is accepted with a 200 state:'active' receipt but served by no read door #9454 chose this predicate for. ⚠️ Qualified (A1): that holds by default and deliberately not under the operator hatch — orgScopedWriteRefusal returns early when isOverlayAllowed is satisfied via OS_METADATA_WRITABLE, so a non-overridable type can still land an org-scoped write while the probe reads env-wide. That divergence is the hatch's own stated contract; its refusal message says it "unlocks the write, not the read", and the row it admits is exactly the kind boot hydration walks past.
  • Not achieved by denying everyone: §4's last case pins that a door which already resolved an organization for an overridable type still reaches the org partition.

⇒ Nothing double-scopes and nothing is wrongly denied.

⭐ What moves — four raw-org callers, with the method that establishes the population (B4)

An earlier draft asserted three and named one file. Corrected. Method: git grep -nE 'getMetaItem(Cached)?\s*\(' across the whole repo excluding tests, docs/, content/ and dist/ — 14 invocation sites — then each organizationId argument traced to its source and partitioned into "already organizationIdForMetaRead" versus "raw".

caller type why it moves
runtime/src/domains/meta.ts:703 hard-coded 'object' allowOrgOverride: false — a caller that cannot be right about scope, by construction
runtime/src/domains/meta.ts:745 hard-coded 'object' same
runtime/src/domains/meta.ts:768 singularType off the URL moves for the non-overridable half of what it serves
runtime/src/domains/packages.ts:1239 (applyPublishedSeeds, org from deps.resolveActiveOrganizationId at :472, forwarded at :492-494) 'seed' also non-overridable, so its org-first attempt now reads the env-wide partition directly. It hand-rolls the same fallback as a second attempt (attempts at :1233-1235), so what it used to reach on the second try it now gets on the first

Every other invocation either names no organization at all, or is a REST door that already computed organizationIdForMetaRead — the idempotence legs above are what make those two cases no-ops. getMetaItemCached has exactly one non-test caller, rest-server.ts:5711, which is the gated cached arm.

All four are left as they are: the ruling was callee-side precisely so the next new caller needs no repair.

Tests

packages/metadata-protocol/src/get-meta-item-org-read-gate.test.ts — 14 cases, modelled on the plural door's pin.

  • §1 — the phantom-shadows-live-row case the card exists for. An env-wide object row and an org-scoped phantom of the same (type, name); a caller passing a raw active organization is served the env-wide document, and the phantom's partition is never read. Repeated across every non-overridable declared type, derived from the registry.
  • §2 — precedence preserved. For view, the org row still wins outright, and a key present only on the env-wide row does not appear in the served document — replacement, not a merge. Plus the ?? fallback chain when the org has no overlay of its own.
  • §3 / §4 — the two idempotence legs, over the complete META_URL_TO_SINGULAR ∪ registry-singular population.
  • §5 — the previewDrafts arm gets the same scope, both directions.
  • §0 pins the premises: the org-overridable set, and that object is not in it.

packages/objectql/src/protocol-meta.test.ts — the two singular-verb fixtures in per-organization overlay isolation move from app to view (B1).

app rolled back to allowOrgOverride: false in #6483, so once this verb gates, app never reaches the org partition at all. On app the first fixture asserted the phantom read this PR removes (and went red), and the second asserted a fall-through that could no longer fail. Both now run on view, the whitelisted specimen — the same re-spelling #6190 made one case up and #14683 made one case down — and each gained the call-level assertion its prose used to make only in a comment: the first pins that the env-wide partition is never queried once the org row answers (precedence, not a merge); the second pins that the org partition was queried before the fall-through.

The [#14683] note that predicted "Its two getMetaItem (SINGULAR) siblings above keep app on purpose: that verb is untouched here" is marked superseded in place, with the date and what falsified it.

Non-vacuity, measured rather than asserted (fix committed at a2ff587d4 first, then mutated, then restored):

leg evidence
mutation reached disk second fixture put back on app (row, request and the new call assertion together); injected app request form grep -c → 1, working blob d1f8da070 differs from the HEAD blob f612521db
result under mutation Tests 1 failed | 94 passed (95) — and the failure is the call assertion, AssertionError: expected "vi.fn()" to be called with arguments, not the label assertion, which still passed. That is the vacuity itself, printed: on app the case cannot reach the org partition, so the fall-through it claims to measure is unmeasurable
restore git diff HEAD empty, git status clean, working blob f612521db byte-identical to the HEAD blob
rebuild leg not needed for the mutation itself (it is in the test file, read from source), and the subject it resolves — @objectstack/metadata-protocol via exportsdist/ — was rebuilt at this commit before the run

The earlier ablation of the gate itself still stands from the first round: with const orgId = request.organizationId; restored, 4 failed / 10 passed — §1's three (expected 'org_acme showcase_task' to be 'env showcase_task', the phantom served) and §5's draft arm — while §2/§3/§4 stayed green, as predicted, since they do not depend on the gate.

Verification, at c5c3f062e (the final commit, origin/main merged in)

check result
pnpm --filter @objectstack/objectql exec vitest run 267 files passed; 4593 tests passed, exit 0
… vitest run src/protocol-meta.test.ts 95 passed (95), exit 0 — was 1 failed | 94 passed at 23a2b51c2
pnpm --filter @objectstack/metadata-protocol exec vitest run 158 files passed, 2 skipped; 2349 tests passed, 10 skipped, exit 0
… vitest run src/get-meta-item-org-read-gate.test.ts 14 passed (14), exit 0 — the pin is still 14/14
pnpm --filter @objectstack/metadata-protocol typecheck exit 0 — and tsc --noEmit --listFiles shows both the edited pin test and src/protocol.ts in the program (1 hit each), so this covers them
pnpm --filter @objectstack/objectql typecheck exit 0, including check:test-typecheckOK … 44 file(s) / 242 error(s) / 69 pinned signature(s), shrink-only ledger unmoved
pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' 71 successful, 71 total
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no path arguments) 43 families, derived from the merged tree with no STALE TREE warning; 41 exit 0
check-test-completeness.mjs, pm/check-half-states.mjs exit 3 = PREREQUISITE NOT MET, recorded as NOT MEASURED on the gates' own instruction — the first needs a saved turbo run test log, the second needs repo-scoped REST egress this container does not have. Neither is a finding
check:dual-build-cjs-loads, check:type-check-debt exit 3 before the build, exit 0 after it — 102 require entry points across 66 packages load; 20 ledger entries re-measured, 405 raw errors, none above its recorded number
eslint . --no-inline-config (full repo, not narrowed) exit 0, no output
node scripts/pm/check-governed-merges.mjs --test on the final 4-file list 0 of 4 path(s) hit the register — not governed
check:nul-bytes plus a manual control-byte scan of all four files clean (grep -naP exit 1, no matches)

Exit codes were captured by redirecting to a file before reading it, never through a pipe. The gate union was run at this commit, and the two suites above were re-run on it after the merge.

Scope

Four files. packages/spec is untouched — the review confirmed it needs no change — including the organizationId describe() wording tracked separately on #14772, which is not addressed here and stays open.

Filed while working this, out of scope for this PR: #14907getMetaItemLayered is the same series' third instalment, reached ungated from runtime/src/domains/meta.ts:346. It is deliberately not folded in: its caller population is different, so this PR's idempotence proof does not cover it, and its orgId binds before the canonical fold, so this one-liner does not port. Its body's "six plugin-security call sites" was corrected to five — the actual non-test getMetaItemLayered( invocations are packaged-permission-set-lock-gate.ts:82 and permission-set-projection.ts:770, :938, :995, :1406; permission-set-overlay-discard.ts:243 only feature-detects and delegates.

Changeset

patch, on @objectstack/metadata-protocol — a behaviour correction on an existing published read door. Semver verified against b6c769019, 26f3588fb, 67ceb9aef, #14767's own changeset and AGENTS.md, and unchanged this round. node scripts/check-changeset-no-major.mjs and node scripts/check-empty-changeset.mjs both exit 0.

Two sentences moved this round: the plural gate is now credited to #14683 / PR #14767 landing in the same release (verified on origin/main: .changeset/getmetaitems-org-read-gate.md is still present and unconsumed, packages/metadata-protocol/package.json is 17.2.0, and its CHANGELOG carries no #14683/#14767 entry), and the restart sentence is corrected as in §The defect, measured.

Review

⚠️ needs:contract-review travels from the card to this PR and is cleared only at reviewer PASS — not by this seat. Left as a draft, auto-merge not armed, not marked ready.

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

Generated by Claude Code

…data registry

The singular `/meta` read verb applied no organization gate of its own: whatever
`organizationId` arrived was spent on whatever `type` arrived. Its two overlay
reads combine with `??` — precedence, not the plural verb's union — so an
ungated organization could SUBSTITUTE rather than merely add: on a type the
registry declares `allowOrgOverride: false`, a pre-#6190 phantom org-scoped row
was served instead of the live env-wide document.

Resolve the scope through `organizationIdForMetaRead` once, after the canonical
type fold, for both the active-overlay read and the ADR-0033 `previewDrafts`
read. ADR-0005's overlay-wins precedence is deliberately unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions github-actions Bot added the size/l label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/data-api.mdx (via /:object/export (route, bridged from symbol getMetaItem — its registrar handler names it))
  • content/docs/data-modeling/drivers.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/data-modeling/schema-design.mdx (via /:object/export (route, bridged from symbol getMetaItem — its registrar handler names it))
  • content/docs/kernel/services-checklist.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/permission-sets.mdx (via /:object/export (route, bridged from symbol getMetaItem — its registrar handler names it))
  • content/docs/protocol/objectui/actions.mdx (via /:object/export (route, bridged from symbol getMetaItem — its registrar handler names it))
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 — 9 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 3210b3d5da5fe0e87c1aabd74f0e117c805ab5b9packageMentionDocs.

Which tree this was computed on

This run read content/docs from ffe379404e79dda697e5e378fe2d8e7687ffddf1 — the merge of head c5c3f062eeb5b7bf6405a715e2c7250ba1a420bf into base 3210b3d5da5fe0e87c1aabd74f0e117c805ab5b9, 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 ffe379404e79dda697e5e378fe2d8e7687ffddf1 && git checkout ffe379404e79dda697e5e378fe2d8e7687ffddf1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3210b3d5da5fe0e87c1aabd74f0e117c805ab5b9 c5c3f062eeb5b7bf6405a715e2c7250ba1a420bf && git checkout -B drift-repro 3210b3d5da5fe0e87c1aabd74f0e117c805ab5b9 && git merge --no-ff c5c3f062eeb5b7bf6405a715e2c7250ba1a420bf

node scripts/docs-audit/affected-docs.mjs --json 3210b3d5da5fe0e87c1aabd74f0e117c805ab5b9

⚠️ 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 3210b3d5da5fe0e87c1aabd74f0e117c805ab5b9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…verridable type

`getMetaItem` now resolves its own read scope through
`organizationIdForMetaRead`, so on `app` — `allowOrgOverride: false` since
#6483 — the org partition is never queried. The two `per-organization overlay
isolation` fixtures that exercised the singular verb on `app` were therefore
measuring the phantom read this branch removes: one went red, and its sibling
stayed green while asserting a fall-through that could no longer fail. Both move
to `view`, the whitelisted specimen, matching the re-spellings #6190 and #14683
already made in the same block, and each gains the call assertion that makes its
claim non-vacuous.

Prose corrections in the same edit, from the contract review:
- the changeset no longer says the plural gate shipped "in the previous
  release" — both changesets are unconsumed, so both land in the same one;
- the phantom row is never deleted, so a restart does not clear the
  substitution; what a restart drops is the row's registry presence;
- the "what moves" enumeration names four raw-org callers across two files,
  with the method that establishes the population;
- the write-side read/write-scope identity is qualified under the
  `OS_METADATA_WRITABLE` operator hatch, and the ADR-0005 / ADR-0029 citations
  are narrowed to what those documents actually decide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

hotlong commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Contract review (re-review after the fix round): PASS — head c5c3f062e, CONTRACT_REVIEW_TIER review by the director seat

Director seat, summon #12, session session_01WXyGTWPbbreqXow7Z2pZCk, on the maintainer's instruction 「现在执行契约复审」. The engine seat's isolated re-review could not run (429, comment 5525655441 on #14770); this is the take-over the 2026-08-31 ruling reserves for the director seat on stall. Fuse: served claude-fable-5-1 = CONTRACT_REVIEW_TIER.

① Derived judgments

  • One line moves the contract: const orgId = organizationIdForMetaRead(request.type, request.organizationId), placed after canonicalizeMetaRequestType and spent by both the previewDrafts arm and the active-overlay arm. Verified in the diff: the ?? precedence read is untouched, and the gate sits after the fold — the two properties the earlier FAIL round checked line by line.
  • Accept set: unchanged. Public surface: unchanged (no new symbol, packages/spec untouched).
  • Behaviour change, bounded by the idempotence argument: f(t, undefined) === undefined and f(t, f(t, o)) === f(t, o) make every already-gated door and every org-less caller a no-op; what moves is a raw active organization handed to a non-overridable type, which now reads the env-wide row instead of a phantom. The four raw-org callers are enumerated with the method that produced the list (grep of every getMetaItem(/getMetaItemCached( site, argument traced) — the repair B4 asked for.
  • The fix round is verified against the diff, not the report. B1: both singular-verb fixtures in protocol-meta.test.ts moved from app to view and were strengthened in opposite directions — case 1 now asserts the env-wide partition is never queried once the org row answers, case 2 asserts the org partition was queried before the fall-through; the #14683 sentence they falsified is marked SUPERSEDED in place. B2: the changeset now says the plural gate lands in the same release. B3: the restart sentence is corrected in both places and re-derived (nothing deletes the phantom row). The vacuity was shown by mutation (label assertion passes, call assertion fails on app) — that is the right evidence.

② Semver vs changeset

@objectstack/metadata-protocol patch — a behaviour correction on an existing published read door, no new symbol. Consistent with the four landed precedents the earlier review cited.

③ Boundary flags

getMetaItemLayered (#14907) is deliberately not folded in — different caller population, orgId binds before the fold, so the one-liner does not port. The organizationId describe wording (#14772) stays open. Both correct.

CI on c5c3f062e: every check green (36 runs). check-clause2-carriers.mjs --pair NOT MEASURED here (HTTP 403, exit 3); the pair (PR + #14770) was read from the API by hand.

Landing: needs:contract-review stripped from this PR and from #14770, marked ready, auto-merge armed. Not governed (0 of 4 paths).


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review September 3, 2026 14:26
@hotlong
hotlong enabled auto-merge September 3, 2026 14:27
@hotlong
hotlong added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit d5cbb44 Sep 3, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

3 participants