Repository navigation
feat(metadata-protocol,objectql,rest,spec)!: every metadata read is environment → code; legacy organization rows and sealed overlays are reported at boot, not served (#15206 S5) - #22628
Conversation
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR Co-authored-by: Claude <noreply@anthropic.com>
…nt → code Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR Co-authored-by: Claude <noreply@anthropic.com>
…egacy organization rows Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
…own combinators; ledgers follow the deleted pins (#15206) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
…, not served (#15206) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
…s no overlay, not four named ones (#15206) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
📓 Docs Drift CheckThis PR changes 7 package(s): 45 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 148 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2db2bac9f2dc19ac9b397963f575f8e5c3611c88 && git checkout 2db2bac9f2dc19ac9b397963f575f8e5c3611c88
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ee3ae0360dc5beff655e4feb8f9624c731e306fd 40e6b5d6d75908b295545c97f414f9affc117088 && git checkout -B drift-repro ee3ae0360dc5beff655e4feb8f9624c731e306fd && git merge --no-ff 40e6b5d6d75908b295545c97f414f9affc117088
node scripts/docs-audit/affected-docs.mjs --json ee3ae0360dc5beff655e4feb8f9624c731e306fd
|
…t rest, not served (#15206) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
Contract reviewServed-tier: Inputs: card #15206 (body and all 45 comments; bound to the stage plan 6067844889, stage 0's S5 row 6067752061, triage's Q2 B / Q3 A 6068032120 and Q1 → C 6073941543, S4's review 6090242635 and the S5 claim 6092019285; the S5 os-dev-report 6094700038); #22011 (body and its ruling A, 6020163868); the 2026-08-24 permission-set ruling as ① Derived judgmentsGate verdicts on this head, as read. Every required context is Every read that stops serving a row, each read off the net diff and judged.
Nothing is deleted or rewritten. No added line in the diff calls a store write or delete ( ② Semver level
ADR-0087: one marker,
③ Boundary flagsDev deviations and open questions, from the S5 report 6094700038, each answered:
For the seat before the landing (none a verdict ground):
Verdict grounds, all on this head: every required context is green; every read enumerated in the stage plan's S5 row stops serving a legacy organization row and the sealed overlays, with the one form-door exception Q3 A keeps and the one permission exception the 2026-08-24 ruling keeps; both boot reports fire, at the right level, and delete nothing; the spec narrowings, the ADR-0087 entry and the changeset's level and arm are judged sound above; the carried S4 items are closed; the claim's ⛔ Not list holds. What remains is a pure regeneration of one committed projection, declarations and follow-ups — the seat's steps, not defects in the diff. Implemented-by: VERDICT: PASS |
… type, not an erasure (#15206) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
…-reads-env-only # Conflicts: # packages/spec/spec-changes.json
Contract reviewServed-tier: Inputs: card #15206 (body and all 48 comments; bound to the stage plan 6067844889, stage 0's S5 row 6067752061, triage's Q2 B / Q3 A 6068032120 and Q1 → C 6073941543, S4's review 6090242635, the S5 claim 6092019285, the first S5 os-dev-report 6094700038 and the patch round 1 report 6097121217; also the seat's status note 6095810656 and the ① Derived judgmentsGate verdicts on this head, as read. Every required context is Why 6095742860 does not carry, and what the delta did. Merge 1 ( Every read that stops serving a row, each read off the net diff and judged.
Nothing is deleted or rewritten. No added line in the net diff calls a store write or delete ( ② Semver level
ADR-0087: one marker,
③ Boundary flagsEvery item 6095742860 raised for the seat, answered on this head:
Dev deviations, open questions and out-of-scope findings, both reports, each answered:
For the seat before the landing (none a verdict ground):
Verdict grounds, all on this head: every required context is green; the two merges are the clean three-way merge and the ruled deletion, with every regenerated artifact green on its gate; every read enumerated in the stage plan's S5 row stops serving a legacy organization row and the sealed overlays, with the one form-door exception Q3 A keeps and the one permission exception the 2026-08-24 ruling keeps; both boot reports fire, at the right level, bounded, and delete nothing; the spec narrowings, the ADR-0087 registration on the no-committed-projection basis, and the changeset's level and arm are judged sound above; every item 6095742860 raised is answered or overtaken; the claim's ⛔ Not list holds. What remains is an external status to read, one cheap confirmation, declarations and follow-ups — the seat's steps, not defects in the diff. Implemented-by: VERDICT: PASS |
维护者速读
改了什么:ADR-0131 C5 的 S5 阶段。这是 C5 最后一个代码阶段,元数据从此只剩「环境 → 代码」两层。
为什么改:ADR-0131 D6 规定环境元数据属于整个部署。S3、S4 已经让写入拒绝组织,读侧却还在按组织合并。S5 让读写一致,组织层从此彻底退场。 风险与代价(含回滚):
席位意见:建议批准。
你要做的:用 |
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.tsmerged clean; #22591's create-remedy change kept whole);spec-changes.jsonregenerated in6c7c6c28d(it then carriedmetadata-read-organization-scope-retired); the legacy-row boot report counts the three ledgers and projectssys_metadatato the four printed columns, pinned with its ablation (82e2de41b, the cast dropped inb45b42a6c); theenvironment-variables.mdxmerge-at-read sentence made true (62f64d223). main merged again at40e6b5d6dbecause #22638 had landed and left the PR conflicted: the modify/delete onpackages/spec/spec-changes.jsonkeeps 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:
_lockgate'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.servedOverlayRowCandidates/findServedOverlayRowand the overlay cache key[type, packageId].ItemAddresslosesorganizationId, and the ETag validator is locale plus content.OS_METADATA_WRITABLEbefore the seal.declinesStoredRowgains one arm,isUnservedSealedOverlay. The package's definition wins.objectsuch row is not loaded at boot either, so the engine keeps the packaged schema.single, Studio saves are stored organization-scoped today. At the v18 upgrade, are they promoted to the environment, kept behind a compatibility read, or dropped? #22011 A).[metadata_org_scoped_unserved]covers the legacy organization rows (active and draft), plus the legacy rows ofsys_metadata_commit/_history/_audit. It namesos 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.resolveFormBySlugandfindPublicFormViewkeep reading the Default Organization's legacyviewlayer, fail-closed.legacyFormOrganizationIdinstead oforganizationId.viewalone. No spec request declares it.rest/src/public-form-withdrawal.test.tsand in dogfood (below).@objectstack/objectqlbinds the environment's authored hook and action rows only. Before,readAuthoredHookRows/readAuthoredActionRowsbound every organization's rows.organizationIdleavesGetMetaItems,GetMetaItem,GetMetaItemLayered,AuditMetaItem,HistoryMetaItemandGetMetaItemCached, and theListDraftsResponsedrafts.overlayScopeis'env' | null.metadata-read-organization-scope-retired.authorable-surface/api.jsonlines for the six vanished keys were removed by hand, following S4's route: the build andgen:authorable-surface-baseboth refuse a vanished key.Carried findings
organizationIdspread into the authoring gateMetadataAuthoringGateContext.organizationIdis gone; no gate read itconfig.organizationIdorganization_idon the record first (a package-published seed has no caller organization)listCommits' organization branchorganization_id IS NULLcommits only, for every caller; legacy commits are counted in the boot reportmetadata-lifecycle.mdx"Per-org overlay" wordingmetadata-service.mdxis alignedisLegacyOrganizationRow: a where-clause spelling trips the federated-readers gate)declaresOrgOverride/organizationIdForMetaReadcallersmetadata-core/src/meta-write-org-scope.tsand its test)organizationIdpermission-set-projection.ts/permission-set-overlay-discard.ts(dropped); the rest form doors (now the legacy key); the rest audit door (organizationId: nulldropped); runtimedomains/packages.ts(comments only)Cross-lane paths
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.tscontent/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.jsonpackages/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.tspackages/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.tsCensus (Q1 → C: census first)
Measured on the two seeded example apps, booted fresh with their seeds by a throwaway
@objectstack/verifyprobe at HEAD. The probe was not committed. It counted every row of the four ledger tables through the engine:sys_metadatarows_commit/_history/_auditlegacy rows@objectstack/example-crm)examples/app-showcase)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
2ccf45701throughscripts/ablation-replace.mjs(WRAP mode, anchor hit ×1 and verified on disk). The subject ismetadata-protocol/src, imported from source by the pin, so nodistleg applies.protocol.ts)organization_id: candidate.organizationIdfromfindServedOverlayRow's where (blob5c3dc429→647a3a8a)|| this.isUnservedSealedOverlay(type, name)fromdeclinesStoredRow(blob5c3dc429→47977733)git checkout HEADby absolute path5c3dc429,git diff HEADempty,ABLATION-marker count 0; pin 20 passedThe 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-registrywas rerun, speccheck:generated --fixreported all 15 artifacts up to date, and spec'ssrc/migrations+scriptstests passed 1,733. Each heavy run went throughscripts/pm/os-verify-lock.sh.@objectstack/metadata-corepnpm --filter … test@objectstack/metadata-protocolpnpm --filter … test@objectstack/objectqlpnpm --filter … test@objectstack/restpnpm --filter … test@objectstack/runtime--shard=1/2,2/2@objectstack/plugin-security@objectstack/spec--shard=1/3,2/3,3/3protocol.reads-environment-only.test.ts@objectstack/dogfoodNote 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-codemanaged-content-sealed,email-template-overlay-survives-boot,datasource-*(×2)permission-set-discard-overlay-eligibilityshowcase-public-form-withdrawal,showcase-public-form-withdrawal-layers,public-form-withdrawal-walledshowcase-object-extension-scalar-divergence,external-validate-sees-runtime-save,package-first-authoring,object-designer-field-reorderThe 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 objectshowcase_accountbeing 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 --commandsderived 134 families at5616042ea, the same set as one commit earlier. All 134 ran on that tree, each exit code captured before any pipe.Reconciliation:
--ranprints134 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'sfinddouble now applieslimit.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 90andcheck-plugin-teardown-shape --self-testrefused on the shallow clone. After deepening they measured, and passed.check:skill-examples,check:dual-build-cjs-loadsandcheck:i18nrefused at first because packages had nodist. 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 themeta-write-org-scopehelper and its test), plus the organization arms of re-premised pins. Breakdown: 1,768 added + 4,420 deleted = 6,188 at40e6b5d6d; 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:LayeredDiff.tsxrenderstranslateConsoleValue('layer', layered.overlayScope ?? 'set'). After S5 the server never emits'org', so theorglabel is dead but harmless.packages/data-objectstack/src/metadata-client.ts:374derivesMetadataOverlayScopefromGetMetaItemLayeredResponse['overlayScope'].packages/app-shell/src/views/metadata-admin/i18n.ts:7151declaresLAYER_SCOPE_ZHas aRecordkeyed by the non-nullMetadataOverlayScopevalues, with anorgkey. Once objectui moves to this spec, that literal'sorgkey is an excess property (a typecheck error).overlayScope: 'org':metadata-client.layeredEnvelope/layeredRoute,ResourceEditPage.resetVerdictandStudioDesignSurface.listViewInspector-11823.No objectui edit here.
Acceptance notes
objectoverlay 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 automation: atkernel:readythe flow sync re-arms a stored row's body over the loader's for a packaged flow name, after the boot pull armed the loader's, so the stored body runs while the receipt says the package's is armed #20913 precedent) and declined at read.revertCommitof a legacy organization commit is still refused 403 by S4's gate (not 404).organizationIdoption stays, used only bylegacyOrganizationRepofor the uninstall removal.next).@objectstack/runtimeis 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