|
| 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