Repository navigation
Commit c8b062f
Refs #15206 (S5)
Clause-②: no (narrowing) — every metadata read is environment → code; no
read refused today is admitted.
**Patch round 1** (2026-10-10T11:39Z), answering contract review
6095742860 and the docs-drift note 6094691685: main merged at
`d5dac9ba0` (`protocol.ts` merged clean; #22591's create-remedy change
kept whole); `spec-changes.json` regenerated in `6c7c6c28d` (it then
carried `metadata-read-organization-scope-retired`); the legacy-row boot
report counts the three ledgers and projects `sys_metadata` to the four
printed columns, pinned with its ablation (`82e2de41b`, the cast dropped
in `b45b42a6c`); the `environment-variables.mdx` merge-at-read sentence
made true (`62f64d223`). main merged again at `40e6b5d6d` because #22638
had landed and left the PR conflicted: the modify/delete on
`packages/spec/spec-changes.json` keeps main's deletion (the file is now
generated at publish only).
**Stage S5 of #15206 only.** #15206 remains open: S6 and the column /
index / history re-keys (#15211, triage ruling Q2 B) are not in this PR.
## What changes
ADR-0131 D6 retired the per-organization overlay axis. S3/S4 stopped the
doors carrying an organization and made the protocol refuse every
organization-scoped write. This PR makes every protocol **read**
environment → code too:
- **Reads are env-only.** The item, list, layered, cached (ETag),
history, diff, audit, drafts and commit-timeline reads now serve the
environment's stored row, else the code packages' definition, whatever
organization the caller names. So do the `_lock` gate's overlay layer,
search's page sweep, diagnostics, references and boot hydration. A row
stored organization-scoped by an earlier release is served by no read.
- The rule lives in one place: `servedOverlayRowCandidates` /
`findServedOverlayRow` and the overlay cache key `[type, packageId]`.
- `ItemAddress` loses `organizationId`, and the ETag validator is locale
plus content.
- **Sealed managed overlays are not served (triage ruling Q1 → C).**
This covers an environment row overlaying an item a managed package
ships, on a type sealed against overlays: any type whose registry entry
admits no overlay (a flow, action, hook, object or datasource, for
example). Such a row was written through `OS_METADATA_WRITABLE` before
the seal.
- `declinesStoredRow` gains one arm, `isUnservedSealedOverlay`. The
package's definition wins.
- An `object` such row is not loaded at boot either, so the engine keeps
the packaged schema.
- **Permission sets are carved out.** A stored fork of a code-declared
permission set keeps its 2026-08-24 ruling: detection reading plus
Discard Overlay, "not a silent migration".
- **Boot reports both populations, once each, per type.** Nothing is
deleted or rewritten (#22011 A).
- `[metadata_org_scoped_unserved]` covers the legacy organization rows
(active and draft), plus the legacy rows of `sys_metadata_commit` /
`_history` / `_audit`. It names `os migrate` (ADR-0131 D10).
- `[metadata_sealed_overlay_unserved]` covers the sealed overlays and
names the two remedies: re-express as a linkage-free clone, or delete
the row.
- **Anonymous form doors are unchanged (triage ruling Q3 A).**
`resolveFormBySlug` and `findPublicFormView` keep reading the Default
Organization's legacy `view` layer, fail-closed.
- The rest door now sends the protocol-internal
`legacyFormOrganizationId` instead of `organizationId`.
- The protocol honours that key for `view` alone. No spec request
declares it.
- The behaviour is pinned unchanged in
`rest/src/public-form-withdrawal.test.ts` and in dogfood (below).
- **`@objectstack/objectql`** binds the environment's authored hook and
action rows only. Before, `readAuthoredHookRows` /
`readAuthoredActionRows` bound every organization's rows.
- **Spec (narrowing).**
- `organizationId` leaves `GetMetaItems`, `GetMetaItem`,
`GetMetaItemLayered`, `AuditMetaItem`, `HistoryMetaItem` and
`GetMetaItemCached`, and the `ListDraftsResponse` drafts.
- `overlayScope` is `'env' | null`.
- New ADR-0087 semantic entry
`metadata-read-organization-scope-retired`.
- The `authorable-surface/api.json` lines for the six vanished keys were
removed by hand, following S4's route: the build and
`gen:authorable-surface-base` both refuse a vanished key.
### Carried findings
| Finding | Outcome |
|:--|:--|
| Dead `organizationId` spread into the authoring gate | removed:
`MetadataAuthoringGateContext.organizationId` is gone; no gate read it |
| Seed loader's refusal prescribed `config.organizationId` | now
prescribes `organization_id` on the record first (a package-published
seed has no caller organization) |
| `listCommits`' organization branch | **report, don't serve**: it lists
`organization_id IS NULL` commits only, for every caller; legacy commits
are counted in the boot report |
| `metadata-lifecycle.mdx` "Per-org overlay" wording | rewritten; the D6
callout is extended; a sealed-overlay paragraph is added;
`metadata-service.mdx` is aligned |
| objectql reads every organization's hook/action rows | environment
rows only (a row-value check, `isLegacyOrganizationRow`: a where-clause
spelling trips the federated-readers gate) |
| `declaresOrgOverride` / `organizationIdForMetaRead` callers | **none
left; deleted** (`metadata-core/src/meta-write-org-scope.ts` and its
test) |
| Read callers outside the claim passing `organizationId` |
plugin-security `permission-set-projection.ts` /
`permission-set-overlay-discard.ts` (dropped); the rest form doors (now
the legacy key); the rest audit door (`organizationId: null` dropped);
runtime `domains/packages.ts` (comments only) |
### Cross-lane paths
- **domain:cli**: `packages/rest/src/execctx-consumer-census.test.ts`,
`packages/rest/src/meta-item-layered-route.test.ts`,
`packages/rest/src/meta-object-extension-property-classes.test.ts`,
`packages/rest/src/meta-object-overlay-extension-fold.test.ts`,
`packages/rest/src/public-form-withdrawal.test.ts`,
`packages/rest/src/rest-server-audit-org-scope.test.ts`,
`packages/rest/src/rest-server-meta-cached-etag-door-scope.test.ts`,
`packages/rest/src/rest-server.ts`,
`packages/runtime/src/audit-meta-item-org-scope.integration.test.ts`,
`packages/runtime/src/domains/domain-protocol-handle-typing.test.ts`,
`packages/runtime/src/domains/meta-read-org-scope-parity.test.ts`,
`packages/runtime/src/domains/packages-protocol-handle-typing.test.ts`,
`packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts`,
`packages/runtime/src/domains/packages.ts`,
`packages/runtime/src/meta-write-org-scope.test.ts`,
`packages/runtime/src/package-list-commits-org-scope.integration.test.ts`,
`packages/runtime/src/package-revert-stored-members.integration.test.ts`
- **domain:devx**: `content/docs/concepts/metadata-lifecycle.mdx`,
`content/docs/kernel/contracts/metadata-service.mdx`,
`content/docs/references/api/protocol.mdx`,
`packages/qa/dogfood/test/showcase-object-extension-scalar-divergence.dogfood.test.ts`,
`scripts/engine-double-contract.pinned.json`,
`scripts/objectql-double-limit.baseline.json`
- **domain:services**:
`packages/plugins/plugin-security/src/permission-set-overlay-discard.ts`,
`packages/plugins/plugin-security/src/permission-set-projection.test.ts`,
`packages/plugins/plugin-security/src/permission-set-projection.ts`
- **domain:spec**: `packages/spec/authorable-surface/api.json`,
`packages/spec/src/api/meta-item-response-shapes.test.ts`,
`packages/spec/src/api/protocol.test.ts`,
`packages/spec/src/api/protocol.zod.ts`,
`packages/spec/src/migrations/entries/semantic/18.metadata-read-organization-scope-retired.ts`,
`packages/spec/src/migrations/registry.ts`
## Census (Q1 → C: census first)
Measured on the two seeded example apps, booted fresh with their seeds
by a throwaway `@objectstack/verify` probe at HEAD. The probe was not
committed. It counted every row of the four ledger tables through the
engine:
| App | `sys_metadata` rows | legacy organization rows | env rows
carrying a package | `_commit` / `_history` / `_audit` legacy rows |
|:--|--:|--:|--:|--:|
| CRM (`@objectstack/example-crm`) | 0 | 0 | 0 | 0 / 0 / 0 |
| Showcase (`examples/app-showcase`) | 0 | 0 | 0 | 0 / 0 / 0 |
**What this shows.** A freshly seeded deployment carries neither
population. Nothing is dropped, and nothing in a fresh install changes
what it serves.
**What it does not show.** Real deployment databases are not reachable
from this container, so their counts are **NOT MEASURED**. The boot
report this PR adds is the census instrument for them. It prints both
populations per type on the first boot of this build, before anyone acts
on a row. No row is touched, which honours "nothing is dropped by guess"
and #22011 A.
**Both reports fired correctly in the dogfood run**, on fixtures that
plant the rows:
- `[metadata_org_scoped_unserved] … flow×1
(showcase_task_assigned_notify@org_dogfood_21002)`
- `[metadata_sealed_overlay_unserved] … object×1 (showcase_account)`,
`flow×1 (showcase_urgent_task_alert)`, `datasource×2 (showcase_external,
default)`
## Reverse verification
Both ablations ran from committed state `2ccf45701` through
`scripts/ablation-replace.mjs` (WRAP mode, anchor hit ×1 and verified on
disk). The subject is `metadata-protocol/src`, imported from source by
the pin, so no `dist` leg applies.
| # | Mutation (`protocol.ts`) | Expected | Observed |
|:--|:--|:--|:--|
| 1 | drop `organization_id: candidate.organizationId` from
`findServedOverlayRow`'s where (blob `5c3dc429` → `647a3a8a`) | §1 red |
**10 failed / 10 passed**: every §1 case (5 tier-A types × 2 caller
shapes) |
| 2 | drop `\|\| this.isUnservedSealedOverlay(type, name)` from
`declinesStoredRow` (blob `5c3dc429` → `47977733`) | §2 red | **2 failed
/ 18 passed**: the managed action and managed hook cases |
| restore | `git checkout HEAD` by absolute path | green | blob == HEAD
`5c3dc429`, `git diff HEAD` empty, `ABLATION-` marker count 0; pin **20
passed** |
The direction was the usual one: red under each mutation, green after
restore.
## Tests
The package suites ran on `4560ed20`, which carries the final source.
The later commits touched no runtime source: the new pin's fake engine
(`2ccf4570`; pin rerun **20 passed**), one dogfood case (`ea26adb03`;
rerun **7/7 passed**), and the ADR-0087 entry / changeset / docs wording
of the seal's reach (`5616042ea`). For that commit,
`gen:migration-registry` was rerun, spec `check:generated --fix`
reported all 15 artifacts up to date, and spec's `src/migrations` +
`scripts` tests passed **1,733**. Each heavy run went through
`scripts/pm/os-verify-lock.sh`.
| Package | Command | Result |
|:--|:--|:--|
| `@objectstack/metadata-core` | `pnpm --filter … test` | 16 files,
**384 passed** |
| `@objectstack/metadata-protocol` | `pnpm --filter … test` | 219 files
(3 skipped), **28,029 passed** / 19 skipped |
| `@objectstack/objectql` | `pnpm --filter … test` | 392 files, **7,729
passed** |
| `@objectstack/rest` | `pnpm --filter … test` | 268 files, **4,973
passed** / 326 skipped |
| `@objectstack/runtime` | vitest `--shard=1/2`, `2/2` | **2,979 + 2,680
passed** |
| `@objectstack/plugin-security` | vitest (full) | 192 files, **4,033
passed** (see note 1) |
| `@objectstack/spec` | vitest `--shard=1/3`, `2/3`, `3/3` | **7,146 +
6,092 + 6,630 passed** |
| new pin `protocol.reads-environment-only.test.ts` | vitest | **20
passed** |
| typecheck | the seven packages above, plus `@objectstack/dogfood` |
all exit 0 |
| dogfood (17 files that read metadata layers) | vitest | **100 passed**
after one case was re-premised (see note 2) |
**Note 1 — plugin-security first run.** It failed 9 files, each with
"Cannot find package '@objectstack/spec/…'", while a concurrent gate
rebuilt spec's `dist`. No test body ran. The rerun on the same tree was
green.
**Note 2 — dogfood.** The 17 files are:
- `flow-shipped-name-*` (×4), `flow-unshipped-name-layered-code`
- `managed-content-sealed`, `email-template-overlay-survives-boot`,
`datasource-*` (×2)
- `permission-set-discard-overlay-eligibility`
- `showcase-public-form-withdrawal`,
`showcase-public-form-withdrawal-layers`,
`public-form-withdrawal-walled`
- `showcase-object-extension-scalar-divergence`,
`external-validate-sees-runtime-save`, `package-first-authoring`,
`object-designer-field-reorder`
The first run had **1 failed / 99 passed**:
`showcase-object-extension-scalar-divergence` › "SHOULD: a tenant's own
rename reaches the reads its forms derive from". That case pinned a
pre-seal environment rename of the **packaged** object
`showcase_account` being served, which is exactly what triage ruling Q1
→ C stops. It is re-premised, not weakened: the row is still at rest and
shown by `?layers=true`, the item and list reads serve the package's
label, and boot names the row. The file then ran **7/7 passed**.
**Q3 A holds.** The three public-form withdrawal dogfood files are
green.
## Gates
`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived **134** families at `5616042ea`, the same set as one
commit earlier. All 134 ran on that tree, each exit code captured before
any pipe.
**Reconciliation:** `--ran` prints `134 derived, 134 run, 0
NOT-MEASURED, 0 UNRUN`. The zero is derived from the recorded codes:
every family exited 0.
**Repaired along the way:**
- `check:engine-double-contract`: the ledger is rewritten with
`--write`. Two pins left with the deleted files, and the new pin's three
doubles were added.
- `check:objectql-double-limit`: two baseline entries removed for files
that are gone or now clean. The new pin's `find` double now applies
`limit`.
- `check:where-matcher`: the new pin's matcher now refuses any
combinator or operator it does not implement.
**Gates that refused at first:**
- `check-engine-split-ratio --days 90` and `check-plugin-teardown-shape
--self-test` refused on the shallow clone. After deepening they
measured, and passed.
- `check:skill-examples`, `check:dual-build-cjs-loads` and `check:i18n`
refused at first because packages had no `dist`. Once the dogfood
closure was built they measured, and passed.
**Notes the gates print themselves, with exit 0:**
- `check-spec-docblock-symbol-anchors --self-test`: "second reality pin
NOT TAKEN" (the objectui sha is not in this clone).
- `check:entry-nameability`: "NOT MEASURED: no callable export on
@objectstack/spec/api-assembled, @objectstack/spec/qa". It prints this
regardless of this diff.
**Repo-wide lint** (`pnpm lint`) was not run locally; that run belongs
to CI.
## Size route: human merge
The diff is 6,188 changed lines across 71 files, over
`HUMAN_MERGE_LINE_THRESHOLD` = 3000
(`scripts/pm/check-governed-merges.mjs`), so this PR takes the **human
route**. About 70% of the size is deletion: whole test files whose
subject S5 retires (five organization-read-gate and ETag-scope pins, and
the `meta-write-org-scope` helper and its test), plus the organization
arms of re-premised pins. Breakdown: 1,768 added + 4,420 deleted =
**6,188** at `40e6b5d6d`; source +615/−1,249, tests +1,023/−3,114,
docs+changeset +66/−25, generated/ledgers +64/−32.
## objectui at the pinned `.objectui-sha` (report only; carrier C9)
At `47b1f0bb71`:
- **Live behaviour.** `LayeredDiff.tsx` renders
`translateConsoleValue('layer', layered.overlayScope ?? 'set')`. After
S5 the server never emits `'org'`, so the `org` label is dead but
harmless.
- **Type break on the next spec bump.**
`packages/data-objectstack/src/metadata-client.ts:374` derives
`MetadataOverlayScope` from
`GetMetaItemLayeredResponse['overlayScope']`.
`packages/app-shell/src/views/metadata-admin/i18n.ts:7151` declares
`LAYER_SCOPE_ZH` as a `Record` keyed by the non-null
`MetadataOverlayScope` values, with an `org` key. Once objectui moves to
this spec, that literal's `org` key is an excess property (a typecheck
error).
- **Tests to update with that bump.** Several test fixtures spell
`overlayScope: 'org'`: `metadata-client.layeredEnvelope/layeredRoute`,
`ResourceEditPage.resetVerdict` and
`StudioDesignSurface.listViewInspector-11823`.
No objectui edit here.
## Acceptance notes
- **Boot layering.** ADR-0029 D9's boot layering of a sealed `object`
overlay is no longer applied. The packaged schema stands, and the row is
named in the sealed report. Sealed rows of other types are still
hydrated as bare tenant copies (the #20913 precedent) and declined at
read.
- **Legacy commit revert.** `revertCommit` of a legacy organization
commit is still refused 403 by S4's gate (not 404).
- **Repository option kept.** The repository's `organizationId` option
stays, used only by `legacyOrganizationRepo` for the uninstall removal.
- **Changeset.** It is minor (pre mode, tag `next`).
`@objectstack/runtime` is not in it: its source change is comments only.
## 维护者速读(草稿)
**改了什么**:元数据的所有读取口(单项、列表、分层、ETag
缓存、历史、差异、审计、草稿、提交时间线、锁、搜索、引用、启动加载)一律只读「环境 →
代码」两层,不再读任何组织级旧行。旧版本存下的组织级行、以及在封存前通过 `OS_METADATA_WRITABLE`
写下的「托管包内容的覆盖行」(flow/action/hook/object/datasource
等不允许覆盖的类型),都不再被使用,启动时按类型各报一次,并给出处理办法。公开表单的匿名入口照旧读默认组织的旧 view 行(裁决 Q3 A)。
**为什么改**:ADR-0131 D6 已退役按组织覆盖元数据的能力,写入口在 S3/S4
已全部拒绝组织级写入;如果读取口还继续提供旧的组织行,退役的那一层就会按调用方逐个"复活",不同组织看到不同的元数据。托管包内容已封存,读取也必须让包定义胜出。
**风险与代价(含回滚)**:依赖旧组织级覆盖行的部署,升级后这些定制不再生效,回到环境行或包定义(启动日志会逐条点名,数据一行未删,可在
v18 迁移仪式中提升,或在 Studio
重新保存为环境级)。依赖封存前覆盖托管内容的部署,同样回到包定义,需改为另起名的克隆。权限集的存储分叉不受影响。回滚:回退本 PR
即恢复原读取行为,无数据迁移需要撤销。objectui 下次升级 spec 时需删掉 `overlayScope` 的 `org` 键(一处类型
+ 若干测试)。
**席位意见**:
**你要做的**:本 PR 超过 3000 行门槛,走人工合并:请审阅后人工批准合并。重点看:权限集是否应被排除在"封存覆盖不提供"之外;封存
object 覆盖行不再在启动时叠加到引擎这一行为变化是否可接受。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0fea05f commit c8b062f
71 files changed
Lines changed: 1768 additions & 4420 deletions
File tree
- .changeset
- content/docs
- concepts
- deployment
- kernel/contracts
- references/api
- packages
- metadata-core/src
- metadata-protocol/src
- objectql/src
- plugins/plugin-security/src
- qa/dogfood/test
- rest/src
- runtime/src
- domains
- spec
- authorable-surface
- src
- api
- migrations
- entries/semantic
- scripts
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
49 | | - | |
| 48 | + | |
| 49 | + | |
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| |||
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
109 | | - | |
| 109 | + | |
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
| |||
142 | 142 | | |
143 | 143 | | |
144 | 144 | | |
| 145 | + | |
| 146 | + | |
145 | 147 | | |
146 | 148 | | |
147 | 149 | | |
| |||
176 | 178 | | |
177 | 179 | | |
178 | 180 | | |
179 | | - | |
| 181 | + | |
180 | 182 | | |
181 | 183 | | |
182 | 184 | | |
183 | 185 | | |
184 | 186 | | |
185 | | - | |
| 187 | + | |
186 | 188 | | |
187 | 189 | | |
188 | 190 | | |
| |||
236 | 238 | | |
237 | 239 | | |
238 | 240 | | |
239 | | - | |
| 241 | + | |
240 | 242 | | |
241 | | - | |
| 243 | + | |
242 | 244 | | |
243 | 245 | | |
244 | 246 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
306 | 306 | | |
307 | 307 | | |
308 | 308 | | |
309 | | - | |
| 309 | + | |
310 | 310 | | |
311 | 311 | | |
312 | 312 | | |
| |||
0 commit comments