Skip to content

Commit 9632604

Browse files
os-muskclaude
andauthored
fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type (#14767)
* wip: apply the registry read gate inside getMetaItems (#14683) Snapshot before verification. `organizationIdForMetaRead(request.type, request.organizationId)` is resolved once, after the canonical fold, and spent by both the active-overlay arm and the previewDrafts arm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 * test(metadata-protocol): pin the getMetaItems org read gate (#14683) Includes the idempotence proof the direction-A ruling was conditional on, mechanised over the complete accepted-spelling population. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 * test(metadata-protocol): re-spell the org-scope cache-key case onto an overridable type (#14683) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 * test(objectql): re-spell the getMetaItems org-union case onto an overridable type (#14683) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 * chore: changeset for the getMetaItems org read gate (#14683) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 * chore: keep the changeset's semver reasoning clear of breaking-change signal tokens (#14683) The body cites #13925's major-plus-marker precedent to REFUSE it. Spelled with the literal markers, check-adr-0087-registration reads the citation as this changeset's own declaration and demands an ADR-0087 disposition for a change that retires nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 * test(metadata-protocol): widen the derived overridable set to string[] for the probe sites (#14683) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 * chore: level this change patch, on its own lineage (#14683) Contract review returned FAIL on the semver level alone. `minor` was derived from #13973, which is open, `pm:blocked`, never landed, and is about date-materialising drivers — it says nothing about read doors. The real precedents are this predicate's own lineage, and all three shipped `patch`: b6c7690 (#9454/#9727) added the org partition to every `/meta` read door; 26f3588 (#10340/#10519) moved which partition two spellings read; 67ceb9a (#11553) repaired the same on the dispatcher. Withholding a partition from types that never had a read channel is the same class. Also corrects the #13925 claim this file's previous wording-repair commit introduced: #13925 shipped `"@objectstack/core": minor` with a bolded banner and an `adr-0087:` marker, not a major bump — `check-changeset-no-major.mjs` refuses `major` outright, so `minor` in this repo MEANS breaking, which is why claiming it here would be wrong. ⛔ No code, test or pin changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 * chore: correct the affirmed consequent in this changeset's semver note (#14683) I wrote that `minor` "means breaking" here, and that claiming it would signal an incompatibility. That affirms the consequent: breaking => minor holds, minor => breaking does not. check-changeset-no-major.mjs says so in the header I cited — during the window the bump level "tells a consumer nothing about whether the release breaks them", and the carriers are the banner and the ADR-0087 disposition, "the only signal there is". The landed record carries the other half: 87ad30c ('@objectstack/types': minor) and 3c1bbd2 ("@objectstack/metadata": minor) are new-export bumps with zero banner tokens. So `minor` is the union, not a breaking marker. Corrected to what the gate actually says: the level carries nothing either way, this change owes neither carrier, and `patch` follows from the lineage and from no export being added. Lines 54-58 unchanged. ⛔ No code, test or pin changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent db07f69 commit 9632604

5 files changed

Lines changed: 652 additions & 11 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata-protocol): `getMetaItems` applies the registry read gate itself, so a sweep that reads more than one type per request is scoped per type (#14683)
6+
7+
`getMetaItems` applied no organization gate of its own: whatever `organizationId`
8+
arrived was spent on whatever `type` arrived. The scope of a metadata sweep was
9+
therefore decided per type **by the caller** — which a request carrying one
10+
organization can only get right when it sweeps **one** type. It now resolves
11+
`organizationIdForMetaRead(request.type, request.organizationId)` once, after the
12+
canonical type fold, and both the active-overlay read and the `previewDrafts`
13+
read spend that one resolution.
14+
15+
Three live callers sweep more than one type and could not have been right:
16+
17+
- `getMetaDiagnostics` with no `type``targetTypes` is the whole registry, the
18+
five `allowOrgOverride: true` types and every other declared type together,
19+
under one request-level organization.
20+
- `findReferencesToMeta``request.type` is the **target**; the organization is
21+
spent on `matcher.fromType`, the **sources**, so the target's own registry flag
22+
says nothing about the types actually read.
23+
- the runtime's package export sweep (`assemblePackageManifest`) — every plural
24+
key of `PLURAL_TO_SINGULAR`, with one raw active organization.
25+
26+
**The harm class is resurrection, not concealment**, and which one it is decides
27+
that the registry-gated predicate is the right instrument.
28+
`SysMetadataRepository.history()` filters `organization_id` by strict equality,
29+
so naming the tenant *there* hides an `allowOrgOverride: false` type's rows. On
30+
this path the two `queryByOrg` reads are UNIONed, so naming it can only **add**
31+
and what it adds are the pre-#6190 phantoms: org-scoped rows of types with no
32+
per-org read channel, which `loadMetaFromDb` walks past and
33+
`reportUnhydratableOrgScopedRows` exists to warn about. Read back, they surface
34+
in the admin "Used by" panel and the Studio governance directory, inside a
35+
clearance rendered before a destructive action — where a resurrected row is worse
36+
than an omission because it reads as evidence.
37+
38+
**Why `patch`, from this change's own lineage.** A published `/meta` read door's
39+
row set changing is not a new class here — it is the class this predicate was
40+
born in, and all three landed instances shipped `patch`:
41+
42+
| commit | what changed | level |
43+
|:--|:--|:--|
44+
| `b6c769019` (#9454 / #9727) | the row set every `/meta` read door returns — org rows **added** | `metadata-core`, `metadata-protocol`, `rest`: all `patch` |
45+
| `26f3588fb` (#10340 / #10519) | which partition two spellings read — rows **moved** | `rest`, `metadata-core`: `patch` |
46+
| `67ceb9aef` (#11553) | the same fold-before-scope repair on the dispatcher door | `runtime`: `patch` |
47+
48+
The first of those is the commit that introduced `organizationIdForMetaRead`
49+
itself. Adding the org partition to every read door was `patch`; moving which
50+
partition two spellings read was `patch`; this change — withholding the org
51+
partition from types that never had a read channel for it — is the same class,
52+
one verb further in, and takes the same level.
53+
54+
⛔ Not `minor`, and in this repo that is a statement rather than a rounding
55+
choice. `scripts/check-changeset-no-major.mjs` refuses `major` outright, so
56+
during the launch window a genuinely breaking change ships as `minor` (pre-1.0,
57+
whole-stack lockstep) — #13925 is exactly that, `"@objectstack/core": minor`
58+
carrying a bolded incompatibility banner and an `adr-0087:` marker for a
59+
narrowed published accept set. But the implication runs ONE WAY ONLY, and the
60+
gate's own header is explicit that it does: during the window `minor` is the
61+
union of ordinary new-functionality bumps and banner-marked breaking ones
62+
(`87ad30c10`, `3c1bbd2a8` are new-export `minor`s carrying no banner at all),
63+
so the bump level "tells a consumer nothing about whether the release breaks
64+
them". The carriers of breaking-ness are the bolded banner in the body and the
65+
ADR-0087 disposition — "during the window they are the only signal there is".
66+
67+
⇒ So `minor` here would not claim an incompatibility; it would claim NOTHING
68+
about compatibility, which is precisely the cost the header names. This change
69+
carries neither carrier because it owes neither — nothing is retired, no accept
70+
set narrows, and `check-adr-0087-registration` reads it as non-breaking. The
71+
level is `patch` because the lineage above is `patch` and no export is added,
72+
not because `patch` rebuts something `minor` would have asserted.
73+
74+
**Nothing here is incompatible, and the reason is what the withheld rows are.**
75+
They are the #6190 phantoms: org-scoped rows of types with no per-org read
76+
channel. The platform has refused to mint them since `ac244ad09` / `6155c3c24`,
77+
boot hydration skips them, `reportUnhydratableOrgScopedRows` audits them, and
78+
**every REST `/meta` read door has already withheld them since `b6c769019`**.
79+
The only doors still serving them were the dispatcher list
80+
(`runtime/src/domains/meta.ts:921`) and the runtime manifest and publish-flip
81+
reads (`packages.ts:1160`, `:603`) — so this change aligns those three with the
82+
published `/meta` surface rather than departing from it. A consumer reading
83+
those rows was reading through a door inconsistent with `/meta`, on data the
84+
platform had already ruled dead.
85+
86+
⛔ Not "only a refactor of where the predicate lives" either: the predicate's new
87+
position does change which rows three doors serve. That is why this is a
88+
behaviour entry rather than an internal note — and, per the lineage above, why
89+
the level for it is `patch`.
90+
91+
**Callers that already gate are unaffected, and that is proved rather than
92+
asserted.** `organizationIdForMetaRead` answers either its argument or
93+
`undefined`, so a second application over the same type is a no-op; the load-
94+
bearing half is that it *is* the same type. The REST `GET /meta/:type` list door
95+
gates on `canonicalMetaUrlType(req.params.type)` and passes the raw segment, which
96+
`canonicalizeMetaRequestType` folds through the identical map — the identical
97+
string. The other four `organizationIdForMetaRead` call sites in `rest-server.ts`
98+
reach `getMetaItemLayered` / `getMetaItem` / `historyMetaItem` / `diffMetaItem`
99+
and never this method. `get-meta-items-org-read-gate.test.ts` §3 measures both
100+
halves over the complete accepted-spelling population (61 spellings, derived from
101+
`META_URL_TO_SINGULAR` unioned with the registry) rather than a hand-listed
102+
sample.

0 commit comments

Comments
 (0)