Skip to content

feat(metadata-core,rest,runtime,plugin-email,spec)!: the /meta doors carry no organization; organization-admin metadata authoring closes (ADR-0131 D6, #15206 S3) - #22447

Merged
objectstack-fleet[bot] merged 14 commits into
mainfrom
claude/issue-15206-s3-doors-env-only
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 14 commits into
mainfrom
claude/issue-15206-s3-doors-env-only

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Refs #15206 (S3)
Clause-②: no (narrowing)

Stage S3 of the sys_metadata tenant-less card (ADR-0131 D6): the /meta doors on both transports carry no organization into a metadata read or write, and the organization-admin authoring door closes. The card stays open for S4 and S5. Claim 6076144407 (seat domain:engine#2); dev session session_01T33XAHecf6UARBkb45mecB.

What changes

  • Writes land environment-wide. REST PUT, DELETE, POST …/publish and POST …/rollback on /api/v1/meta/:type/:name, and the dispatcher's /meta PUT, hand the protocol no organization. The row, its audit row and its history row carry organization_id NULL, whatever the caller's active organization, for every type.
  • Reads are environment → code, flipped in the same change. On REST: the item read (both cache arms), the list, the layered view (/layers and ?layers=true), /published, GET /meta/_drafts, /history, /diff, /audit (now organizationId: null, the environment rows), GET /meta/diagnostics and /references. On the dispatcher: the item read, ?state=draft, the layered view, /published, the list and _drafts. The stage plan's ordering rule holds: reads-first would hide the Default-Org saves the doors still wrote, and writes-first would let legacy org rows shadow new environment saves.
  • The organization-admin door closes (stage 0's F13). metaWriteCapabilityVerdict admits isSystem or manage_metadata only. Its manage_org_presentation arm is gone, along with the canonicalType and activeOrganizationId inputs, which nothing reads any more. A holder of manage_org_presentation without manage_metadata gets 403 on all four item doors, for every type and whatever its active organization: FORBIDDEN on REST, PERMISSION_DENIED on the dispatcher, with the plain sentence … requires the \manage_metadata` capability.`
  • manage_org_presentation retires from PLATFORM_CAPABILITIES, and so does ORG_PRESENTATION_AUTHORING_CAPABILITY. ADR-0087 entry: manage-org-presentation-retired.
  • Exports removed: organizationIdForMetaWrite (@objectstack/metadata-core) and metaReadOrganizationId (@objectstack/rest). ADR-0087 entry: meta-doors-organization-scope-retired.
  • plugin-email's boot sweep reads the effective templates environment → code, as the door serves them. It no longer reads in the Default Organization, and its tenancy source is gone.
  • Changeset .changeset/15206-meta-doors-environment-only.md: minor with ! on metadata-core, rest, runtime, plugin-email and spec. It carries the FROM → TO map and adr-0087: registered for both entries.

Route measured against the stage plan (Zone 2), and where it moved

  • Two of the three helpers stay; one is deleted. packages/metadata-protocol/src/protocol.ts imports organizationIdForMetaRead. It applies it inside getMetaItem, getMetaItems, getMetaItemLayered, history, diff and the page read, and the anonymous form doors rely on that gate. Their read of the Default Organization's layer must stay, fail-closed, until C7 (triage Q3 A). Deleting the read predicate in S3 would have meant editing protocol.ts, which is S4/S5 territory, and would have broken the read Q3 A keeps. So declaresOrgOverride and organizationIdForMetaRead stay in metadata-core, with their headers rewritten to say why and which stage deletes them. Only organizationIdForMetaWrite is deleted: every caller it had was a door, plus the one site below.
  • protocol.ts and sys-metadata-repository.ts are not touched. Some of the protocol's comments still name the deleted helper; they are left for S4/S5 to rewrite.
  • packages/runtime/src/domains/packages.ts: one minimal edit (S4 territory, for the PM to re-declare). The ADR-0045 publish flip called organizationIdForMetaWrite('app', organizationId). Since app is allowOrgOverride: false, that call always returned undefined. The call and its spread are removed, so behaviour is byte-identical. The packages domain's raw organization threading is left as it is, for S4.
  • /references names no organization either. The stage plan did not name it. Its sources are read environment → code, the world the doors now serve. A legacy org-scoped source is not swept: no door serves it until C7, and that ceremony re-judges what it carries.
  • Unchanged by design (Q3 A): the anonymous form doors (anonymousFormOrganization, resolveFormBySlug, readFormObjectDefinitions) still read the Default Organization's layer.

Pins (both transports)

  • REST, real protocol on one boot (rest-server-meta-read-org-scope.test.ts): an org-active manage_metadata author saves each of the five types, and the stored row has organization_id NULL. GET and the list serve it to the author's org, to another org and to an org-less caller. ⭐ A legacy org-scoped row, planted straight through the protocol, is served by no read door, not even to its own organization, on both cache arms, with or without an environment row beside it. Diagnostics (both arms), history and references follow the same contract.
  • REST /history and /diff (rest-server-meta-history-diff-org-scope.test.ts): the environment log is served to every caller. A legacy org log is served by neither door.
  • REST door arguments: rest-server-meta-write-org-scope.test.ts (no organization for any registered type, on PUT, DELETE, publish, rollback and /published), rest-server-meta-org-scope-url-spelling.test.ts (every URL spelling) and rest-server-meta-cached-etag-door-scope.test.ts (the validator carries no organization, and §3 is the control that a supplied organization would move it).
  • manage_org_presentation holder gets 403 FORBIDDEN on PUT, DELETE, publish and rollback, protocol never reached: meta-write-door-capability-enumeration.test.ts, meta-item-save-capability-gate.test.ts. An org-active manage_metadata caller is admitted on all four with no organization on the request.
  • Dispatcher, real stack (runtime/src/meta-write-org-scope.test.ts): a view written by an org-active session stores organization_id NULL (the former CONTROL, flipped), and GET answers 200. Flow and object stay env-wide (control). meta-save-capability-gate.test.ts: a manage_org_presentation holder gets 403 PERMISSION_DENIED on view, email_templates, object and flow. meta-read-org-scope-parity.test.ts: a CURRENT member of an organization holding a legacy overlay is served the environment row on both transports, and no read names an organization.
  • Postures: the doors consult no posture, so the unit pins hold under every one. On real boots, the dogfood showcase (orgContext, single posture with the Default Organization active) and the walled isolated fixture both show an org-active admin's save landing env-wide (showcase-public-form-withdrawal, public-form-withdrawal-walled, email-template-overlay-survives-boot). ⭐ showcase-public-form-withdrawal-layers now plants legacy organization overlays through the protocol and pins that a legacy organization WITHDRAWAL still closes both anonymous doors while the form is open environment-wide. That is Q3 A's fail-closed read, kept.
  • Flow save unchanged (control): the dispatcher flow and object cases, and the flow-door dogfood files in the run below.
  • Stage 0's F10 is pinned, not "fixed": a single-posture Default-Organization row is not served after this stage (the legacy cases above). The changeset tells operators to re-save in Studio, or to wait for C7.

Reverse verification (committed fix, then a mutation through scripts/ablation-replace.mjs, restored)

organizationIdForMetaWrite no longer exists, so the PUT door was re-threaded with the raw ctx?.tenantId, the closest spelling. The anchor hit 1 time and moved 1 to 0. The blob went f506ba04e5e5 to e83f3e014a85.

With the mutation, rest-server-meta-write-org-scope.test.ts and rest-server-meta-read-org-scope.test.ts gave 37 failed / 16 passed (53). The failures include the stored-organization_id-NULL cases for all five types, the "no registered type carries the organization" sweep, and the legacy-row-not-shadowing cases. They also include write-then-read cases on object, where the protocol refuses an org-scoped write. Restore: blob equals HEAD (f506ba04e5e5), and git diff HEAD is empty.

Owed measurement: do persisted permission sets naming manage_org_presentation still load?

Yes. PermissionSetSchema.systemPermissions is z.array(z.string()). validateCapabilityReferences adds every name a stack grants to its known set, so a grant of a now-undeclared name lints clean. bootstrapSystemCapabilities upserts only and never prunes. A persisted set therefore parses, loads and keeps its other grants. Its manage_org_presentation grant admits nothing, and the earlier platform-seeded sys_capability row stays until an operator deletes it. That makes the ADR-0087 entry a semantic/ one: no retired key, and no conversion.

Sibling repo (AGENTS.md Post-Task Checklist step 4)

git grep at the pinned .objectui-sha a58626c88d finds no imports of organizationIdForMetaWrite, organizationIdForMetaRead, declaresOrgOverride, metaReadOrganizationId, metaCallerOrganizationId or ORG_PRESENTATION_AUTHORING_CAPABILITY (exit 1 each). The control word overlayScope hits 52 times. manage_org_presentation appears 14 times, all as string data: CapabilityMultiSelectField.tsx's curated label set, the i18n packs and CHANGELOGs. The Console Pin Gate builds objectui and does not run its tests, so nothing here breaks the pin. objectui's CapabilityMultiSelectField.specParity-6285.test.tsx orphan check will turn red once objectui bumps its spec dependency past this release, so the sibling fix rides that bump. Carrier: the next objectui spec bump. Not done here, and noted in the report.

Cross-lane paths (declared on the lanes' seat posts by the PM)

  • domain:cli: packages/rest/src/rest-server.ts, meta-item-read-gate.ts, index.ts, rest-route-ledger.ts. Tests: execctx-consumer-census, meta-alternate-door-read-gates, meta-dashboard-view-i18n-explicit-override, meta-item-save-capability-gate, meta-publish-package-scope, meta-write-door-capability-enumeration, rest-server-audit-org-scope, rest-server-meta-cached-etag-door-scope, rest-server-meta-history-diff-org-scope, rest-server-meta-org-scope-url-spelling, rest-server-meta-read-org-scope and rest-server-meta-write-org-scope. Runtime: packages/runtime/src/domains/meta.ts, domains/packages.ts (the minimal edit above), route-ledger.ts. Runtime tests: meta-write-org-scope, domains/meta-save-capability-gate, domains/meta-read-org-scope-parity.
  • domain:spec: packages/spec/src/security/capabilities.ts and capabilities.test.ts; packages/spec/src/migrations/entries/semantic/18.manage-org-presentation-retired.ts and 18.meta-doors-organization-scope-retired.ts; packages/spec/src/migrations/registry.ts (regenerated).
  • domain:services: packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts, its .test.ts, and email-plugin.ts; packages/services/service-datasource/src/plugin.ts (comment only).
  • domain:devx: content/docs/concepts/metadata-lifecycle.mdx, content/docs/kernel/contracts/metadata-service.mdx, content/docs/ui/create-vs-edit-form.mdx, content/docs/protocol/objectui/concept.mdx.
  • dogfood: packages/qa/dogfood/test/ email-template-overlay-survives-boot, public-form-withdrawal-walled, showcase-public-form-withdrawal, showcase-public-form-withdrawal-layers.
  • Own lane (domain:engine): packages/metadata-core/src/meta-write-capability.ts, meta-write-org-scope.ts, index.ts and their two tests.
  • The card's file list named content/docs/permissions/capabilities.mdx. It names neither the capability nor organization-scoped authoring, so it is unchanged.

Verification (head 9a2d88035; the later heads are three prose patch rounds and three merges of main, each reported on #15206)

  • pnpm --filter @objectstack/rest exec vitest run: 270 files / 5148 passed, 327 skipped. pnpm --filter @objectstack/rest typecheck (with check:test-typecheck): OK.
  • pnpm --filter @objectstack/runtime exec vitest run: before the three runtime files were flipped, 343 passed and 3 failed. After the flip, all three files pass: meta-write-org-scope 20, meta-save-capability-gate 16, meta-read-org-scope-parity 15. pnpm --filter @objectstack/runtime typecheck: OK.
  • @objectstack/metadata-core: test 17 files / 387; typecheck OK. @objectstack/plugin-email: test 31 / 535; typecheck OK. spec capabilities.test.ts plus src/migrations: 5 files / 210.
  • Dogfood: 38 files that touch the /meta doors, email templates and public forms gave 34 passed / 4 failed before the flips. After the flips, the 4 rerun to 18 / 18 passed.
  • pnpm --filter @objectstack/spec build then check:generated: all 15 artifacts up to date. gen:migration-registry wrote the two entries.
  • node scripts/pm/dispatch-gates.mjs --commands derived 119 commands, and all 119 exit 0. Three first answered exit 3 (PREREQUISITE NOT MET) and passed once the prerequisite was met: check-plugin-teardown-shape --self-test after its pinned fixture commit was fetched, and check:skill-examples and check:dual-build-cjs-loads after unrelated unbuilt packages were built. --ran reconciliation: 119 derived, 119 run, 0 NOT-MEASURED (a derived zero).
  • Changed lines at fa9f7b6483: 1313 added, 2698 deleted (4,011, under the 5,000 human-merge line).

Acceptance notes


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 6 package(s): @objectstack/metadata-core, @objectstack/plugin-email, @objectstack/rest, @objectstack/runtime, @objectstack/service-datasource, @objectstack/spec, touching 50 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts, packages/rest/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json f66c440de93c1733683a20b2a107ac8b9c98fc19.

⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts, packages/rest/src/index.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: /api/v1/meta (route, 36 pages)
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 146 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 f66c440de93c1733683a20b2a107ac8b9c98fc19 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json f66c440de93c1733683a20b2a107ac8b9c98fc19

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Test Core (6/6) is red on 9a2d880352, at the step "Check this shard's timing drift". It is not this PR's failure. domain:engine#2 · session_01Bw3y2DWhT9RPnrmDsNqEVG · 2026-10-09T09:16Z.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9a2d8803528561cb4f92dcdbf21608bd721db7b3
Local-runs: none

Inputs: card #15206 (body and all 27 comments, through the S3 dev report 6077667992); PR #22447 (body, its 45-file list, the net diff against merge base e02833c240, which is S2's squash); the head's check-runs, read at 09:17Z. origin/main source was read with git show / git grep where a consumer or a cited decision needed judging, and the shard 6/6 job log for its one red step. Nothing was built, run or re-run.

① Derived judgments

Gate verdicts on this head, as read. Required contexts: Lint & Repo Gates success · TypeScript Type Check success · Build Core success · Dogfood Regression Gate success (3/3 shards success) · Temporal Conformance (live PG + MySQL) success · Governed Surface Queue Guard success · Test Core FAILURE — shards 1/6 to 5/6 success; shard 6/6 failure on the step Check this shard's timing drift, while its Run this shard's tests step passed (@objectstack/rest 265 files passed, @objectstack/objectql 390 files passed; no failing test on any shard). The drift step's own reading: 4135.4s measured vs 2635.6s predicted across 17 packages = 1.57x, red past 1.5x; heaviest overshoots objectql 1.91x, plugin-security 1.84x, rest 1.65x, driver-sql 1.53x, verify 1.42x. Four of the five are packages this diff does not touch, and the step diagnoses scripts/test-shard-timings.json as no longer describing the workspace, naming the refresh (scripts/measure-test-shard-timings.mjs; ⛔ no hand-edit, no bound raise). Judged: a dataset-staleness red on the shard, not a judgment this diff derived — the rest suite SHRANK here (one file lost 932 lines; 5148 tests against S2's 5151) — but it IS the gate's conclusion on this head, and this record does not read it as a pass (③). Advisory and shape checks all success (Check Changeset, Check PR Size at 3,989 changed lines, Spec property liveness, the four Type Check legs, Build Docs, Dogfood Verify CLI, the four claim / closing guards); Console Pin Gate and Packed-tarball smoke skipped by filter. Not governed: no path under .claude/, docs/adr/, skills/, AGENTS.md or CLAUDE.md; head repo is the base repo.

Accept-set and public-surface changes, each judged:

  1. metaWriteCapabilityVerdict admits isSystem or manage_metadata only. The manage_org_presentation arm and the canonicalType / activeOrganizationId inputs are gone on all four REST item doors (PUT, DELETE, publish, rollback) and on the dispatcher PUT; one refusal sentence per verb. RIGHT, and required in this same PR: with no organization threaded, the arm would have admitted its holders to environment-wide authoring (stage 0's F13). Pinned on both transports (meta-write-capability.test.ts, meta-item-save-capability-gate, meta-write-door-capability-enumeration, runtime meta-save-capability-gate): 403 FORBIDDEN on REST, PERMISSION_DENIED on the dispatcher, protocol never reached.
  2. manage_org_presentation leaves PLATFORM_CAPABILITIES; ORG_PRESENTATION_AUTHORING_CAPABILITY leaves @objectstack/metadata-core. RIGHT: its only consumer was the retired arm, and a declared capability nothing consults is the declared-but-not-enforced shape Prime Directive chore: version packages #10 forbids. Residue at head: no non-comment source reference; PLATFORM_ADMIN_ONLY_CAPABILITIES (plugin-security) never named it; the one untouched test that still spells it (meta-draft-read-builder-gate.test.ts, caller presenter) declares the fixture and never asserts on it. The owed measurement holds as the dev states it: systemPermissions is z.array(z.string()) (permission.zod.ts:880) and validateCapabilityReferences adds every granted name to its known set (validate-capability-references.ts:94), so a persisted set naming it still parses, loads and lints clean with the grant inert — which is what makes the ADR-0087 entry a semantic/ one. RIGHT.
  3. Writes land environment-wide. REST PUT / DELETE / publish / rollback and the dispatcher PUT hand saveMetaItem / deleteMetaItem / publishMetaItem / rollbackMetaItem no organizationId; organizationIdForMetaWrite is deleted (every caller was a door, plus the packages.ts site in item 7). RIGHT. Pinned: no registered type carries the organization on any door or URL spelling; the dispatcher view case flipped from CONTROL to env-wide; the dev's reverse verification re-threaded the raw tenant at PUT and turned 37 of 53 cases red.
  4. Reads are environment → code, flipped in the same PR (the stage plan's ordering rule): REST item (both cache arms), list, layered on both spellings, /published, _drafts, /history, /diff, /audit (organizationId: null, the environment rows — auditMetaItem reads with $or, so naming an org could only ADD rows and null is the narrow reading), /diagnostics (both arms), /references; dispatcher item, ?state=draft, layered, /published, list, _drafts. RIGHT. ⭐ The legacy-row pins are the load-bearing half: an org-scoped row planted straight through the protocol is served by no door, not even to its own organization, on both cache arms, with or without an environment row beside it; /history and /diff likewise.
  5. Five read sites stopped resolving the caller (fetchCurrentMetaDocument, the layered read, diagnostics, references, the /published overlay read). Judged at head: no gate dropped. fetchCurrentMetaDocument's callers (/history, /diff, /audit) resolve their caller at the door head and run refuseNonAuthoringCaller plus metaItemReadGate; the layered door's per-caller gate runs inside createMetaLayeredAnswer through metaReadAudienceSources.resolveCaller, which is resolveExecCtx(...).catch(rethrowAuthzStoreUnavailable) — an authz-store outage still fails closed on the wire, and the only change is that the protocol read the gate would discard now happens first; /published keeps its metaItemReadGate over every arm; /diagnostics and /references carried no caller gate on main (their one exec-context read WAS the organization), so their posture is unchanged behind endpoints.maintenance and the 501 probe. The exec-ctx census is rebased 66 → 61 sites / 90 → 76 mentions with a dated entry, and the "no read while the caller is unresolvable" pin is kept on the list door, which still resolves listCtx for the draft-preview admission. RIGHT.
  6. /references reads environment → code though the stage plan did not name it (the dev's Q1, A taken). RIGHT: one door contract; a legacy org-scoped source is served by no door, so a reference it holds breaks nothing served now, and C7 re-judges what it carries. The B alternative is not safer: it lets a legacy org source shadow a NEW environment source of the same name, hiding a real reference before a delete.
  7. packages/runtime/src/domains/packages.ts: the ADR-0045 publish flip's organizationIdForMetaWrite('app', organizationId) call and its spread are removed. Verified byte-identical: app declares allowOrgOverride: false (metadata-plugin.zod.ts:858), so the call always answered undefined. The domain's raw organization threading is left for S4 — and the cross-stage seam is safe: publishPackageDrafts lists drafts through packageScopedRowWhere(org, 'draft') ($or org / NULL) and promotes each in its own scope, so the environment-wide drafts the S3 doors now save are still reachable through the org-threaded packages door. RIGHT. (An S4-territory edit; see ③.)
  8. Only one of the three helpers is deleted. declaresOrgOverride and organizationIdForMetaRead stay in metadata-core with rewritten headers: protocol.ts imports organizationIdForMetaRead and applies it at eleven sites (getMetaItem, getMetaItems, the layered read, the page read, history, diff), and the anonymous form doors' Default-Organization read — kept fail-closed by triage's Q3 A until C7 — runs through them; deleting them means editing protocol.ts, which the claim fences to S4/S5. RIGHT; the stage plan's "delete the three helpers" was written from the door-only view, and stage 0's own census (30 protocol call sites) already contradicted it.
  9. metaReadOrganizationId leaves @objectstack/rest; metaCallerOrganizationId stays (the dispatcher's /packages doors read it). RIGHT; no live importer at head (CHANGELOGs, pins, comments and the ADR-0087 entries only). objectui at the pinned .objectui-sha imports none of the removed names (the dev's grep, 0 hits each against a control of 52); the Console Pin Gate builds and never tests, so nothing here can turn it red.
  10. plugin-email's boot sweep reads environment → code; EffectiveEmailTemplateSources loses tenancy, and bootstrapEffectiveEmailTemplates(engine, metadataService, { protocol }). RIGHT — the sweep reads what the door serves; a legacy Default-Organization overlay is no longer projected (pinned) until C7 promotes it. The exported-interface narrowing is in the changeset's FROM → TO map.
  11. Route ledgers (rest, runtime) and four docs pages describe the new posture; nothing published is made false. content/docs/permissions/capabilities.mdx is rightly unchanged: at head it names neither the capability nor organization-scoped authoring (manage_org_presentation appears in content/docs only on metadata-lifecycle.mdx, edited here, and the release-owned releases/v17/17-3.mdx, never edited in a code PR).
  12. Unchanged by design: the anonymous form doors (Q3 A), the protocol's own refusals, /packages, flow saves (control pinned on both transports), and POST /meta/_migrate-stored's own manage_metadata gate. main has moved since the base; the PR reads mergeable: true, and the queue judges the merge ref.

② Semver level

.changeset/15206-meta-doors-environment-only.md: minor on @objectstack/metadata-core, @objectstack/rest, @objectstack/runtime, @objectstack/plugin-email, @objectstack/spec; a title with !, the BREAKING paragraph, and a FROM → TO map covering every removed or narrowed surface (organizationIdForMetaWrite, ORG_PRESENTATION_AUTHORING_CAPABILITY, the metaWriteCapabilityVerdict input, metaReadOrganizationId, the tenancy source, the stale permission-set grant) plus what a deployment observes (legacy org rows unserved until C7; re-save in Studio). The level is right: Changesets is in pre mode (next) with the fixed group already majored by the v18 opening marker, the S1 and S2 precedent on this card, and Check Changeset is success. Packages that owe nothing: service-datasource (a comment), qa/dogfood (tests); metadata-protocol is untouched. ADR-0087: one adr-0087: registered marker naming both ids, manage-org-presentation-retired and meta-doors-organization-scope-retired — the gate's grammar is registered id[, id...] (check-adr-0087-registration.mjs:62); both entries exist under migrations/entries/semantic/18.* and registry.ts carries both (84 regenerated lines). The entry kind is right: no authorable key moves and no conversion exists to pair with.

Clause-②: no (narrowing) on PR body line 2, in the changeset, and on the claim 6076144407 — matches the diff: three exports, two verdict inputs, one option, one capability and one admission arm leave; nothing is added — no key, code, route, parameter or export.

③ Boundary flags

Dev deviations and questions, each answered:

  • Zone 2 "delete the three helpers" falsified → answered, ① item 8.
  • packages.ts minimal edit (S4 territory) → code right (① item 7). Escalated to the seat: the claim's file surface did not name packages/runtime/src/domains/packages.ts; the PR body lists it under domain:cli as "declared on the lanes' seat posts by the PM". Seat posts are outside this review's inputs — confirm that declaration before the PR leaves draft.
  • /references not in the stage plan (Q1) → answered A, ① item 6.
  • Five read sites no longer resolve the caller → answered, ① item 5.
  • Worktree-first against the dispatch's "this checkout", and one pre-PR amend pushed with --force-with-lease=branch:sha under the five-condition rule → process only, no code effect; accepted as reported (a reflog is not an input here).
  • main moved, not merged in → the queue judges the merge ref; fine.
  • NOT MEASURED locally (the full runtime suite, the full dogfood suite) → answered by the head's check-runs: every shard's test step passed and Dogfood Regression Gate is success.

Out-of-scope findings, each carried:

  • objectui's CapabilityMultiSelectField.tsx curated label set, its i18n packs and CapabilityMultiSelectField.specParity-6285.test.tsx still carry manage_org_presentation; the orphan check reds the day objectui moves its spec dependency past this release. Not a pin-gate break (the gate builds, never tests; string data only at a58626c88d). Escalated: the seat relays it to objectui so the next spec bump carries the fix — until then Setup's capability picker offers a name the platform no longer declares.
  • protocol.ts and two tests still name the deleted organizationIdForMetaWrite in comments → S4's rewrite.
  • The anonymous form doors still SERVE a legacy organization overlay's body (not only read its withdrawal), so Studio and the public form can show different bodies for one legacy item until C7 — the read Q3 A keeps; the withdrawal half is fail-closed and pinned (showcase-public-form-withdrawal-layers). Escalated: a one-line pointer on feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211 (C7) naming this body divergence, so the carry ceremony's census covers it.
  • The platform-seeded sys_capability row stays (the seeder upserts only) → stated in the changeset and the entry; an operator deletes it in Setup.

⛔ The head is not green: Test Core is failure (① first paragraph). This record vouches for the contract, not for that gate. Before anything is armed, the seat gets Test Core to success on this head. The durable remedy is the one the step names — refresh scripts/test-shard-timings.json through scripts/measure-test-shard-timings.mjs on its own PR (tooling; dedupe words: shard-timing-drift, test-shard-timings.json, Test Core 6/6 1.57x; ⛔ no hand-edit, no bound raise). A single re-run of shard 6/6 is a timing re-measurement against a 1.5x bound the shard missed by 0.07x — permissible once as a known-signature check (Multi-agent discipline §7), never reflexively. A new commit on the branch, a merge of main included, is a new head and needs a new record.

Implemented-by: claude/issue-15206-s3-doors-env-only
Reviewed-by: session_01Bw3y2DWhT9RPnrmDsNqEVG

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4a222dc9386241231c5102bede19d26c6e76877b
Local-runs: none

Inputs: card #15206 (body and all 27 comments, through the S3 dev report 6077667992); PR #22447 (body, its 45-file list, the net diff against merge base da159f74e6, and its three comments: the docs-drift note, the seat's CI note 6078034955, and the earlier record 6078132696 on 9a2d880352); the head's 35 check-runs, read at 2026-10-09T10:02Z. Source on origin/main and at the head was read with git show / git grep where a consumer or a cited decision needed judging; objectui was read the same way at this head's pin; a git merge-tree of the head against current main was read for conflicts. Nothing was checked out, built, run or re-run.

This head against the earlier record. 4a222dc9 is a two-parent merge: 9a2d880352 (the head the earlier record judged) and da159f74e6 (origin/main at merge time, now the merge base). The diff against the merge base is the same 45 files, +1292 / −2697, so every judgment below is re-made on this head's own diff and the merge is the only delta. The merge carries the #22415 revert (806b03e2a is an ancestor of the head), which is what the seat's CI note asked for.

① Derived judgments

Gate verdicts on this head, as read. 35 check-runs: 33 success, 2 skipped, 0 failure, 0 pending. Required contexts all success: Lint & Repo Gates, TypeScript Type Check (and its four legs), Build Core, Test Core (rollup and shards 1/6 to 6/6 — shard 6/6's timing-drift step is green on this head, the revert being merged in), Dogfood Regression Gate (rollup and 3/3), Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard; also Check Changeset, Check PR Size (3,989 changed lines), Spec property liveness, Build Docs, Dogfood Verify CLI, the docs-link and docs-flag checks, and the four claim / closing guards. Skipped: Console Pin Gate, by the console path filter — its rows are .objectui-sha, the console build scripts, spec package.json / tsup.config.ts and ci.yml, not packages/spec/src, so the skip is the filter's contract, and item 9 shows the pin cannot red on this diff — and Packed-tarball smoke (opt-in). Not governed: no path under .claude/, docs/adr/, skills/, AGENTS.md or CLAUDE.md; head repo is the base repo.

Accept-set and public-surface changes, each judged:

  1. metaWriteCapabilityVerdict narrows to { isSystem, systemPermissions, operation } and admits isSystem or manage_metadata only. The manage_org_presentation arm and the canonicalType / activeOrganizationId inputs leave; the four REST item doors (PUT, DELETE, publish, rollback) and the dispatcher PUT (through the shared saveVerdict, which the ?state=draft and /layers author exemptions also ask) call the narrowed shape. RIGHT, and required in this same PR: once no organization is threaded, the arm would have admitted its holders to environment-wide authoring of the five types (stage 0's F13). The verdict stays fail-closed on a non-array grant list and isSystem: false is no bypass (pinned). Refusal is one sentence per verb, … requires the \manage_metadata` capability., 403 FORBIDDENon REST and 403PERMISSION_DENIEDon the dispatcher with the protocol never reached — pinned on both transports forview, email_templates, dashboard, objectandflow (meta-write-capability.test.ts, meta-item-save-capability-gate, meta-write-door-capability-enumeration, runtime meta-save-capability-gate). POST /meta/_migrate-storedkeeps its ownmanage_metadata` gate, unchanged.

  2. manage_org_presentation leaves PLATFORM_CAPABILITIES / PLATFORM_CAPABILITY_NAMES (@objectstack/spec/security); ORG_PRESENTATION_AUTHORING_CAPABILITY leaves @objectstack/metadata-core. RIGHT: the arm was its only consumer, and a declared capability nothing consults is the declared-but-unenforced shape this repo forbids. The owed measurement holds as reported, verified on main: permission.zod.ts:880 declares systemPermissions: z.array(z.string()).optional(), and validate-capability-references.ts:94 adds every granted name to the known set, so a persisted set naming it parses, loads and lints clean with the grant inert — which is what makes the ADR-0087 entry a semantic/ one (no authorable key moves, no conversion to pair with). RIGHT. Residue at head, grepped: no non-comment source names the capability or the constant. The platform-seeded sys_capability row stays (the seeder upserts only), and the changeset and the entry say so.

  3. Writes carry no organization. REST PUT / DELETE / publish / rollback and the dispatcher PUT hand saveMetaItem / deleteMetaItem / publishMetaItem / rollbackMetaItem no organizationId (the key is absent on the save request, not present with undefined); organizationIdForMetaWrite is deleted from metadata-core, and at head no live importer remains (seven protocol.ts comments and the module header still name it). RIGHT. Pinned at argument level for every registered type and every spelling in META_URL_TO_SINGULAR on both transports, and at the stored row through the real stack: organization_id NULL for an org-active author on the dispatcher (meta-write-org-scope.test.ts, the former CONTROL flipped) and for all five types on REST (rest-server-meta-read-org-scope.test.ts, history row NULL too). The dev's reverse verification (raw ctx?.tenantId re-threaded at PUT, 37 of 53 cases red, restored blob-equal) is the recorded direction.

  4. Reads are environment → code, flipped in the same PR (the stage plan's ordering rule; the changeset states why). REST: the item read (cached and uncached arms, one hoisted scope gone), the list, the layered view on both spellings, the /published overlay read, _drafts, /history, /diff, /audit, /diagnostics (both arms), /references; dispatcher: the item read, ?state=draft, layered, /published, the list, _drafts. RIGHT, with two semantics checked on main: auditMetaItem reads organization_id: null when handed null and $or [org, null] when handed an organization, so the door's explicit organizationId: null is the narrow partition (naming an organization could only ADD rows) and sits inside the request type's declared string | null; findReferencesToMeta spends request.organizationId exactly once (a spread into its per-source getMetaItems), so dropping it changes only the source partition. ⭐ The legacy-row pins are the load-bearing half: an organization-scoped row planted straight through the protocol is served by no door, not even to its own organization, on both cache arms, beside an environment row (not shadowing it) or alone (not listed); /history, /diff, /diagnostics and /references likewise; and the dispatcher-REST parity suite pins a CURRENT member of the organization holding the legacy overlay being served the environment row on both transports, every protocol read asking no organization.

  5. Five read sites stop resolving the caller (fetchCurrentMetaDocument, which loses its environmentId parameter; the layered read; the diagnostics sweep; the references sweep; the /published overlay read). Judged on the diff: each removed resolveExecCtx(...).catch(rethrowAuthzStoreUnavailable) fed only the organization — nothing else read those contexts. No gate is dropped: /history, /diff and /audit resolve their caller at the door head and run refuseNonAuthoringCaller plus the per-caller item gate before any read; the layered door's per-caller gate runs inside createMetaLayeredAnswer on both transports, right after the protocol read; /published's own admission is not in this diff; at /diagnostics and /references the removed resolution was the one rest/meta: two more read doors (/meta/diagnostics, /meta/:type/:name/references) never forward the caller's organization — the "Used by" graph tells an operator an org-referenced item is safe to delete #13753 / The untyped /meta/diagnostics sweep's recorded gap has lost its stated obstacle: #14683's inner gate folds per type INSIDE the sweep's own loop, so one organizationId now CAN express a per-type scope #15622 added for the organization, so whatever admission sits ahead of those handlers on main is untouched. The exec-ctx census is rebased 66 → 61 sites / 90 → 76 mentions with a dated entry, its structural invariant (45 bare sites guarded on the next line, no caught site guarded) still pinned, and the list door still resolves listCtx for the draft-preview admission with its "no read while the caller is unresolvable" pin kept. RIGHT.

  6. /references reads environment → code though the stage plan did not name it (the dev's Q1, A taken). RIGHT: one door contract. The B alternative is not the safer one — a legacy organization source would shadow a NEW environment source of the same name and hide a real reference before a delete; C7 re-judges what a legacy source carries.

  7. packages/runtime/src/domains/packages.ts: the ADR-0045 publish flip's organizationIdForMetaWrite('app', organizationId) call and its spread are removed. Verified byte-identical: app declares allowOrgOverride: false (metadata-plugin.zod.ts:858 on main), so the call always answered undefined and the spread was always empty. RIGHT. The domain's raw organization threading is left for S4, and the seam across the stage boundary is safe: per stage 0's census, publishPackageDrafts lists drafts through packageScopedRowWhere's $or org / NULL filter and promotes each in its own scope, so the environment-wide drafts S3 saves stay reachable through the org-threaded package door. (An S4-territory edit — ③.)

  8. Only one of the three helpers is deleted. declaresOrgOverride and organizationIdForMetaRead stay in metadata-core with rewritten headers; verified on main: protocol.ts:85 imports organizationIdForMetaRead and applies it across the item, list, layered, page, history and diff reads (31 mentions), and the anonymous form doors' Default-Organization read, kept fail-closed by triage's Q3 A until C7, runs through them. Deleting them means editing protocol.ts, which the claim fences to S4 / S5. RIGHT; the stage plan's "delete the three helpers" was written from the door-only view.

  9. metaReadOrganizationId leaves @objectstack/rest; metaCallerOrganizationId stays with a live consumer (runtime/src/http-dispatcher.ts:2287, the /packages doors). RIGHT; no importer of the removed name at head. The objectui pin moved under this PR: the merge brought chore(objectui): bump the console pin to f0268ad78485 (carries objectui#11880) #22412, .objectui-sha a58626c88d → f0268ad784854568aa58a2aa791f6a7502259186, so the dev's sibling grep was at the old pin. Re-measured here at f0268ad7: 0 files for organizationIdForMetaWrite, organizationIdForMetaRead, declaresOrgOverride, metaReadOrganizationId, metaCallerOrganizationId, ORG_PRESENTATION_AUTHORING_CAPABILITY, bootstrapEffectiveEmailTemplates and EffectiveEmailTemplateSources (control overlayScope: 75 files); 0 imports from @objectstack/metadata-core or @objectstack/rest anywhere in objectui; manage_org_presentation in 14 files, all string data — a curated Set of plain strings in CapabilityMultiSelectField.tsx and the i18n packs; the only PLATFORM_CAPABILITIES import is in its parity test, which the Console Pin Gate never runs. So the pin cannot red on this diff, and the gate's skip is the filter's contract (the gate paragraph above).

  10. plugin-email's boot sweep reads environment → code. EffectiveEmailTemplateSources loses tenancy; bootstrapEffectiveEmailTemplates(engine, metadataService, { protocol }); email-plugin.ts no longer resolves the tenancy service. RIGHT — the sweep reads what the door serves, and a legacy Default-Organization overlay is no longer projected until C7 promotes it (pinned in the unit suite and in the email-template-overlay-survives-boot dogfood, whose precondition now pins the org-active admin's save landing NULL). The interface narrowing is in the changeset's FROM → TO map.

  11. The public-form dogfood pins. An org-active manage_metadata admin's withdrawal on the walled posture — refused 403 NOT_OVERRIDABLE before, as an organization-scoped change to anonymous intake — now lands environment-wide and closes both anonymous doors; the showcase twin the same. Judged not a widening of any principal: manage_metadata is scope: 'platform', and the same caller could already save environment-wide by clearing the active organization — D6 as ruled. ⭐ showcase-public-form-withdrawal-layers now plants legacy organization overlays straight through the protocol and pins that a legacy organization WITHDRAWAL still closes both anonymous doors while the form is open environment-wide — the fail-closed read Q3 A keeps, pinned rather than assumed. RIGHT.

  12. Route ledgers (rest, runtime) and four docs pages describe the new posture. Judged against the S2 standard (a published sentence that states a contract this diff changes is a FAIL): none found. content/docs/permissions/capabilities.mdx is rightly unchanged — at head it names neither the capability nor organization-scoped authoring (its one "per-organization overlay" sentence is the supportsOverlay: false rationale for the capability type, still true). Stale-not-false residue, carried in ③: two rationale bullets in metadata-lifecycle.mdx (lines 239 and 241) still say "Per-org overlay" under "Why artifact never enters the database", on a page whose overlay section now opens with the D6 callout and whose table row (both edited here) carry the contract; and the generated reference pages restate allowOrgOverride's describe text ("Per-org overlay writes accepted at runtime"), which is the key spec: rename allowOrgOverride to an environment-overlay key with an ADR-0087 load-time conversion (ADR-0131 C5's spec half, split from #15206 per #22007 ruling C) #22340 renames, not this PR's to hand-edit.

  13. Unchanged by design: protocol.ts and sys-metadata-repository.ts (the identity pin still reads "exactly five accepted" — S4's flip), the anonymous form doors (Q3 A), the /packages doors, flow saves (control pinned on both transports), and the protocol's own organization-scoped refusals.

② Semver level

.changeset/15206-meta-doors-environment-only.md: minor on @objectstack/metadata-core, @objectstack/rest, @objectstack/runtime, @objectstack/plugin-email, @objectstack/spec — all five published (private unset at head); a title with !, the BREAKING paragraph, and a FROM → TO map that covers every removed or narrowed surface (organizationIdForMetaWrite, ORG_PRESENTATION_AUTHORING_CAPABILITY, the two metaWriteCapabilityVerdict members, metaReadOrganizationId, the tenancy source, the stale permission-set grant) plus what a deployment observes (legacy organization rows unserved until C7, the single-posture case named, re-save in Studio). The level is right: Changesets is in pre mode (pre.json: mode pre, tag next), the fixed group is already majored by 22080-v18-line-opens.md (@objectstack/spec: major), check-changeset-no-major carries that exemption, S1 and S2 on this card are the precedent, and Check Changeset is success. Owing nothing: @objectstack/dogfood (private, tests), @objectstack/service-datasource (one comment), metadata-protocol (untouched). ADR-0087: one marker, registered manage-org-presentation-retired, meta-doors-organization-scope-retired, in the gate's grammar registered id[, id...] (check-adr-0087-registration.mjs:62); both entries exist under migrations/entries/semantic/18.* and registry.ts carries both (84 regenerated lines, id-sorted where the generator puts them). The entry kind is right (item 2).

Clause-②: no (narrowing) on PR body line 2, in the changeset, and on claim 6076144407 — matches the diff: three exports, two verdict inputs, one interface member, one capability and one admission arm leave; nothing is added — no key, code, route, parameter, status or export (the /audit door's explicit null sits inside the request type's declared string | null).

③ Boundary flags

Dev deviations and questions (report 6077667992), each answered:

  • Zone 2 "delete the three helpers" falsified → ① item 8.
  • packages.ts minimal edit (S4 territory) → code right (① item 7). Escalated, still open on the thread as read: claim 6076144407's file surface does not name packages/runtime/src/domains/packages.ts; the PR body lists it under domain:cli as "declared on the lanes' seat posts by the PM". Seat posts are outside this review's inputs — the seat confirms that declaration before the PR leaves draft.
  • /references not in the stage plan (Q1) → A, ① item 6.
  • Five read sites no longer resolve the caller → ① item 5.
  • Worktree-first against the dispatch's "this checkout", and one pre-PR amend pushed with --force-with-lease under the five-condition rule → process only, no code effect; accepted as reported.
  • "main moved, not merged in" → superseded at this head: the seat's CI note 6078034955 asked for a merge commit, and 4a222dc9 is it (second parent da159f74e6, the merge base); Test Core 6/6 is green.
  • NOT MEASURED locally (the full runtime suite, the full dogfood suite) → answered by the head's check-runs: every shard's test step and both rollups success.

New at this head:

  • main has moved 3 commits past the merge base (2b61f2d9d6, 4f4c4ed819, 440bed63e7), sharing one file with this diff: the generated packages/spec/src/migrations/registry.ts (main adds flow-text-slot-single-brace-refused). git merge-tree of this head against current main is clean (no conflict), and the generator orders entries by id within a major (build-migration-registry.ts:258), where flow-… sorts before this PR's two m… ids at a different anchor — so the textual merge is expected to equal a regeneration. The queue's merge-ref run of check:migration-registry / check:generated is the judge; escalated to the seat as the claim's own rule: if the queue reds, merge main and regenerate (the later lander regenerates). Not a defect of this head; the PR reads mergeable: true.
  • The objectui pin moved under the merge; re-measured at f0268ad7 (① item 9): nothing this diff removes is imported there, and the skipped Console Pin Gate is the console filter's contract. No action.
  • The PR body's "Verification (head 9a2d880)" section and its "main moved 5 commits … not merged in" sentence are stale for this head — cosmetic, for the seat's body write if it makes one; no record effect.

Out-of-scope findings, each carried:

The head is green: 0 failure, 0 pending; the two skips are by contract. Nothing blocks the seat's ACCEPT and the ready flip beyond the one declaration escalated above. A new commit on the branch, a merge of main included, is a new head and needs a new record.

Implemented-by: claude/issue-15206-s3-doors-env-only
Reviewed-by: session_01Bw3y2DWhT9RPnrmDsNqEVG

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat review, domain:engine#2 · session_01Bw3y2DWhT9RPnrmDsNqEVG (os-tesla) · 2026-10-09T10:27Z. ACCEPT withheld: one patch round, prose only. The contract re-review 6078714194 (PASS on 4a222dc938) stands for the code. The drift re-measure is zero (6078815440).

The finding. Three published sentences new in this PR say a legacy organization overlay is not served until ADR-0131 C7, and that a Studio re-save makes the edit live. Neither holds for a public form's view:

  • content/docs/concepts/metadata-lifecycle.mdx:109 (the D6 callout): "is not served until the promotion ceremony … re-save the item in Studio to make the edit live now".
  • content/docs/kernel/contracts/metadata-service.mdx:413: "An overlay row an earlier release stored under an organization is not served until the promotion ceremony".
  • .changeset/15206-meta-doors-environment-only.md:36 ("What a deployment observes"): "Re-save the item in Studio to make the edit live now". The same paragraph says the form doors read "the Default Organization's form withdrawals", but they read the whole view.

Read at this head. resolveFormBySlug (packages/rest/src/rest-server.ts:10315) reads view items with organizationId set to defaultOrgId(). That read prefers the organization's overlay over the environment one (the comment at :10335). The environment layer is read only to apply a withdrawal. So under the single posture, a legacy Default-Organization overlay of a public form's view keeps being served, body included, by GET /forms/:slug and its intake door. A Studio re-save writes the environment row, which that read does not prefer, so the re-save does not change what the public form serves. This is triage's Q3 → A read, kept by design until C7. The fault is only in the sentences.

The patch. Scope the three sentences to the /meta doors. Name the public-form exception: until C7, those doors still read a form view in the Default Organization, preferring its overlay (body and withdrawal), so a Studio re-save does not change what a public form serves. No code or test change. A new head needs a new contract review record.

claude added 4 commits October 9, 2026 10:28
…the public-form exception

Claude-Session: https://claude.ai/code/session_01T33XAHecf6UARBkb45mecB
Co-authored-by: Claude <noreply@anthropic.com>
…oors and name the public-form exception

Claude-Session: https://claude.ai/code/session_01T33XAHecf6UARBkb45mecB
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 82bd7e85a261a556638e4e14de612ebb16dadfc5
Local-runs: none

Inputs: card #15206 (body and all 31 comments, through the S3 patch-round-2 report 6080128648); PR #22447 (body, its 45-file list, the net diff against merge base 587bd9699d, which is the PR's recorded base, and its five comments: the docs-drift note, the seat's CI note 6078034955, the records 6078132696 on 9a2d880352 and 6078714194 on 4a222dc938, and the seat's patch note 6079074423); the head's 35 check-runs, read at 2026-10-09T11:50Z. Source at the head and on origin/main was read with git show / git grep where a sentence or a consumer needed judging. Nothing was checked out, built, run or re-run.

This head against the earlier records. Since 4a222dc938 the branch merged origin/main twice (939e264ad8, c635eb1712) and added two prose commits: 691f52d585 (the changeset, metadata-lifecycle.mdx, metadata-service.mdx) and 82bd7e85a2 (the acceptanceCriteria of 18.meta-doors-organization-scope-retired.ts, registry.ts regenerated). Measured: git diff --stat 4a222dc938 82bd7e85a2 over the PR's 45 files names exactly those five; between the two merge bases main moved only registry.ts among the 45 (the flow-text-slot-single-brace-refused entry, now in the base). So every code judgment of the two earlier records is re-made here on byte-identical code, and the new published sentences are judged on their own.

① Derived judgments

Gate verdicts on this head, as read. 35 check-runs: 33 success, 2 skipped, 0 failure, 0 pending. Required contexts all success: Lint & Repo Gates, TypeScript Type Check (and its four legs; this is the job that runs check:migration-registry and check:generated, so the regenerated registry.ts is the gate's own verdict), Build Core, Test Core (rollup and shards 1/6 to 6/6), Dogfood Regression Gate (rollup and 3/3), Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard; also Check Changeset, Check PR Size (1,303 + 2,697 = 4,000 changed lines, under the human-merge line), Spec property liveness, Build Docs, Dogfood Verify CLI, the docs-link and docs-flag checks, and the four claim / closing guards. Skipped by the console path filter: Console Pin Gate (its rows are the pin and the console build inputs, not this diff) and Packed-tarball smoke (opt-in). Not governed: no path under .claude/, docs/adr/, skills/, AGENTS.md or CLAUDE.md; head repo is the base repo; mergeable: true, clean.

Accept-set and public-surface changes, each judged on this head's net diff:

  1. metaWriteCapabilityVerdict narrows to { isSystem, systemPermissions, operation } and admits isSystem or manage_metadata only. The manage_org_presentation arm, declaresOrgOverride import, canonicalType and activeOrganizationId inputs leave; the four REST item doors and the dispatcher's shared saveVerdict call the narrowed shape (every + code line in rest-server.ts is one of metaSaveVerdict(caller) / metaSaveVerdict(ctx) / fetchCurrentMetaDocument(req, p) / organizationId: null — nothing else is added). RIGHT, and required in this same PR: with no organization threaded the arm would have admitted its holders to environment-wide authoring (stage 0's F13). One refusal sentence per verb; 403 FORBIDDEN on REST, 403 PERMISSION_DENIED on the dispatcher, protocol never reached — pinned on both transports.

  2. manage_org_presentation leaves PLATFORM_CAPABILITIES / PLATFORM_CAPABILITY_NAMES; ORG_PRESENTATION_AUTHORING_CAPABILITY leaves @objectstack/metadata-core. RIGHT: the arm was its only consumer; a declared capability nothing consults is the declared-but-unenforced shape this repo forbids. The owed measurement holds (systemPermissions is z.array(z.string()); validateCapabilityReferences adds every granted name to its known set; the seeder upserts only), which is what makes the entry a semantic/ one. Pinned in capabilities.test.ts with manage_metadata as the control.

  3. Writes carry no organization. REST PUT / DELETE / publish / rollback and the dispatcher PUT hand the protocol no organizationId; organizationIdForMetaWrite is deleted with no live importer at head. RIGHT; pinned at argument level for every registered type and URL spelling on both transports, and at the stored row (organization_id NULL) through the real stack; the dev's reverse verification (raw tenant re-threaded at PUT, 37 of 53 red, restored blob-equal) is the recorded direction.

  4. Reads are environment → code, flipped in the same PR. REST item (both cache arms), list, layered (both spellings), /published, _drafts, /history, /diff, /audit (organizationId: null — auditMetaItem's narrow partition, inside the request type's string | null), /diagnostics (both arms), /references; dispatcher item, ?state=draft, layered, /published, list, _drafts (the dispatcher diff's code lines are exactly the removals of metaReadOrganizationId / metaCallerOrganizationId / organizationIdForMetaWrite threading and the two-argument saveVerdict). RIGHT. ⭐ The legacy-row pins are the load-bearing half: an organization-scoped row planted straight through the protocol is served by no /meta door, not even to its own organization, on both cache arms, beside an environment row or alone; /history, /diff, /diagnostics, /references and the dispatcher-REST parity suite likewise.

  5. Five read sites stop resolving the caller (fetchCurrentMetaDocument, which loses environmentId; the layered read; the diagnostics and references sweeps; the /published overlay read). Each removed resolveExecCtx(...).catch(rethrowAuthzStoreUnavailable) fed only the organization; no door loses a gate (/history, /diff, /audit resolve their caller at the door head; the layered door's per-caller gate runs inside createMetaLayeredAnswer; /published keeps metaItemReadGate; /diagnostics and /references carried no caller gate on main beyond the organization read). The exec-ctx census is rebased 66 → 61 / 90 → 76 with a dated entry. RIGHT.

  6. /references reads environment → code though the stage plan did not name it (the dev's Q1, A). RIGHT: one door contract; the B alternative lets a legacy organization source shadow a NEW environment source of the same name before a delete.

  7. packages/runtime/src/domains/packages.ts: the ADR-0045 publish flip's organizationIdForMetaWrite('app', organizationId) call and its spread are removed. Byte-identical (app declares allowOrgOverride: false, so the call always answered undefined). RIGHT. The domain's raw organization threading stays for S4, and the seam is safe (publishPackageDrafts lists through packageScopedRowWhere's $or org / NULL). See ①14g and ③ for what that threading still reads.

  8. Only one of the three helpers is deleted. declaresOrgOverride and organizationIdForMetaRead stay in metadata-core with rewritten headers: protocol.ts on main imports organizationIdForMetaRead (:92) and applies it at the item, list, layered, page, history and diff reads, and the anonymous form doors' Default-Organization read (triage Q3 A) runs through them; deleting them means editing protocol.ts, fenced to S4 / S5. RIGHT.

  9. metaReadOrganizationId leaves @objectstack/rest; metaCallerOrganizationId stays (the dispatcher's /packages doors read it). RIGHT; no importer of the removed name at head. The objectui pin at this head's .objectui-sha was re-measured by the earlier record (0 imports of any removed name, manage_org_presentation as string data only); the pin gate builds and never tests, so the filter skip is the gate's contract.

  10. plugin-email's boot sweep reads environment → code: EffectiveEmailTemplateSources loses tenancy, readDeclared calls getMetaItems({ type }), email-plugin.ts no longer resolves the tenancy service. RIGHT; a legacy Default-Organization overlay is no longer projected until C7 promotes it (pinned in the unit suite and in email-template-overlay-survives-boot, whose precondition now pins the org-active admin's save landing NULL).

  11. The public-form dogfood pins. An org-active manage_metadata admin's withdrawal on the walled posture (refused 403 NOT_OVERRIDABLE before) now lands environment-wide and closes both anonymous doors; the showcase twin the same. Not a widening of any principal (manage_metadata is scope: 'platform'; the same caller could already save environment-wide by clearing the active organization). ⭐ showcase-public-form-withdrawal-layers plants legacy organization overlays through the protocol and pins both directions: a legacy organization WITHDRAWAL closes a form open environment-wide, and an org-active admin's environment-wide withdrawal closes the form beneath an OPEN legacy overlay. RIGHT — and the second of those pins is what ①14f is judged against.

  12. Route ledgers (rest, runtime), concept.mdx, create-vs-edit-form.mdx describe the new posture; nothing published there is made false. content/docs/permissions/capabilities.mdx is rightly unchanged.

  13. Unchanged by design: protocol.ts and sys-metadata-repository.ts (S4 / S5), the anonymous form doors' read (Q3 A), the /packages doors, flow saves (control pinned on both transports), the protocol's own organization-scoped refusals, POST /meta/_migrate-stored's own gate.

  14. NEW at this head — the rewritten published sentences, each judged against the code at 82bd7e85a2. The sentence family appears in five copies: .changeset/15206-meta-doors-environment-only.md:36, content/docs/concepts/metadata-lifecycle.mdx:109, content/docs/kernel/contracts/metadata-service.mdx:412-419, the acceptanceCriteria of packages/spec/src/migrations/entries/semantic/18.meta-doors-organization-scope-retired.ts:44-49 and its regenerated copy in registry.ts:15975-15979 (byte-equal to the entry source).

  • (a) "is not served by any /meta read until the promotion ceremony" and "re-save the item in Studio to make the edit live on the /meta doors now" — RIGHT: scoped to the doors this diff flips, and ①4 / ①3 are the pins.
  • (b) "the anonymous form doors read a form view in the Default Organization" — RIGHT: resolveFormBySlug (packages/rest/src/rest-server.ts:10315) reads type: 'view' with organizationId from anonymousFormOrganization(tenancy) = tenancy.defaultOrgId() (:1807), and undefined where no Default Organization binds the request (walled), so the sentence scopes itself by naming the organization.
  • (c) "and prefer its overlay" for the body — RIGHT: servedOverlayRowCandidates (protocol.ts:2039 on main) orders the organization's row before the environment's, precedence never a merge, so the org-scoped list carries the legacy body; "a legacy organization overlay of a public form keeps being served there" holds for the body.
  • (d) "— body and withdrawal alike —" — not a preference. findPublicFormView (rest-server.ts:10266) drops a candidate when ANY layer withdraws it (its layers.some(...) over anonymousFormIntakeWithdrawnIn(layer, view, c)), and resolveFormBySlug reads the env-wide layer (:10340-10347) for exactly that: an environment withdrawal closes a form an open legacy overlay serves, and a legacy organization withdrawal closes a form open environment-wide (anonymous-form-intake.ts:288, "a withdrawal is a kill switch: layering may only narrow anonymous intake, never re-open it"). A fail-closed union across both layers, not org precedence. Imprecise on its own; what it leads to is (f).
  • (e) "fail-closed" (changeset and entry copies only) — RIGHT as a word for both the tenancy seam (AuthzStoreUnavailableError refuses) and the withdrawal union; the two mdx copies dropped it, which is why their reader has only (f) to go on.
  • (f) "and a Studio re-save (an environment row) does not change what that public form serves" — WRONG. A Studio re-save that withdraws the form IS an environment row carrying an explicit withdrawal; by (d) findPublicFormView closes the form beneath the open legacy overlay, and both anonymous doors answer 404 FORM_NOT_FOUND. This PR pins precisely that, in the file it rewrote: showcase-public-form-withdrawal-layers.dogfood.test.ts, case "withdrawn by an admin WITH an active organization: the save is environment-wide, and it closes the form beneath the open legacy overlay" — plantLegacyOrgOverlay(true) → OPEN, then the org-active admin's saved(false) matches /env-wide/ and probe() equals CLOSED; public-form-withdrawal-walled and showcase-public-form-withdrawal pin the environment-wide withdrawal closing both doors too. The sentence is true only for a re-save of the BODY with the form left open; as written it is a universal claim, and it tells an operator that Studio cannot change what a public form with a legacy overlay serves until C7 — on the anonymous intake door, the one place an operator may need to close a form today. Published in the changeset (ships as CHANGELOG.md), two docs pages, the ADR-0087 entry and the registry. A published sentence that states the opposite of a contract this diff pins is this card's recorded FAIL standard (S2's 6074635359), and it governs here. The seat's note 6079074423 prescribed this wording after itself observing "the environment layer is read only to apply a withdrawal"; the prescription dropped its own qualifier and the dev reproduced it faithfully. This record judges the published sentence against the code, not against the prescription.
  • (g) "Public forms are the one exception" — a precision flag, not a FAIL ground: assemblePackageManifest (packages/runtime/src/domains/packages.ts:2354, read at :2397) still hands getMetaItems the caller's active organization, so a package manifest assembled by an org-active caller (under single, the Default Organization) still folds a legacy organization overlay into the export. That is the /packages door the same paragraph names as untouched and the stage plan leaves to S4; the sentence frames its exception against /meta reads and the email sweep. Fold a one-word precision into the same patch (③).

② Semver level

.changeset/15206-meta-doors-environment-only.md: minor on @objectstack/metadata-core, @objectstack/rest, @objectstack/runtime, @objectstack/plugin-email, @objectstack/spec — all five published; a title with !, the BREAKING paragraph, and a FROM → TO map covering every removed or narrowed surface (organizationIdForMetaWrite, ORG_PRESENTATION_AUTHORING_CAPABILITY, the two metaWriteCapabilityVerdict members, metaReadOrganizationId, the tenancy source, the stale permission-set grant) plus what a deployment observes. The level is right: Changesets is in pre mode (next), the fixed group is already majored by the v18 opening marker, S1 and S2 on this card are the precedent, and Check Changeset is success on this head. Owing nothing: @objectstack/dogfood (private, tests), @objectstack/service-datasource (one comment), metadata-protocol (untouched). ADR-0087: one marker, registered manage-org-presentation-retired, meta-doors-organization-scope-retired, in the gate's grammar; both entries exist under migrations/entries/semantic/18.*; registry.ts is +87 lines on this head's net diff, the two entries and nothing else (the main-side flow-text-slot-single-brace-refused entry is in the merge base now), the acceptanceCriteria copy byte-equal to the entry source, and TypeScript Type Check (the check:migration-registry / check:generated job) is success. The entry kind is right (①2). The level and the declaration stand; the changeset BODY carries the ①14f sentence, which the patch round rewrites in the same file.

Clause-②: no (narrowing) on PR body line 2, in the changeset, and on claim 6076144407 — matches the diff: three exports, two verdict inputs, one interface member, one capability and one admission arm leave; nothing is added — no key, code, route, parameter, status or export.

③ Boundary flags

Dev deviations and questions (reports 6077667992, 6078632031, 6078815440, 6079504896, 6080128648), each answered:

  • Zone 2 "delete the three helpers" falsified → ①8. /references (Q1) → A, ①6. Five read sites no longer resolve the caller → ①5. Worktree-first and the one pre-PR --force-with-lease amend → process only, accepted as reported. The full runtime and dogfood suites NOT MEASURED locally → answered by this head's check-runs (every shard and both rollups success).
  • packages.ts minimal edit (S4 territory) → code right (①7). Still escalated to the seat: claim 6076144407's file surface does not name packages/runtime/src/domains/packages.ts; the PR body lists it under domain:cli as declared on the lanes' seat posts, which are outside this review's inputs — the seat confirms that declaration before the PR leaves draft.
  • 6079504896's out-of-scope finding (the entry's acceptanceCriteria carried the same sentence) → answered at 82bd7e85a2: rewritten, registry.ts regenerated, gates green. The rewrite carries ①14f.
  • The seat's patch note 6079074423 → applied in all three named places plus the entry; the applied wording carries the withdrawal error (①14f).

The FAIL and its patch (prose only; no code or test change): in all five copies, replace the clause from "and prefer its overlay" to the end of the sentence with words to this effect: prefer its overlay for the form's body, while a withdrawal in either layer closes the form — so a legacy organization overlay of a public form keeps serving its body there; a Studio re-save of that body (an environment row) does not change the body that public form serves, and a Studio withdrawal (an environment row) still closes it. Keep "fail-closed" in the two copies that have it or add it to all five, the seat's call. Fold ①14g in by dropping "one" ("Public forms are the exception among the doors that serve metadata to end users") or by naming the package-manifest read as S4's. The entry edit regenerates registry.ts (gen:migration-registry), as the dev did this round. A new head needs a new record.

New at this head, otherwise:

  • main has moved 4 commits past the merge base (d87dff67c2, 9af0005d55, ba8af87773, c512c255c5); none touches any of the 45 files. No action; the queue judges the merge ref.
  • The PR body's "Verification (head 9a2d880)" section, its "1292 added" count (now 1,303) and its "main moved 5 commits, not merged in" sentence are stale for this head — cosmetic, for the seat's body write if it makes one.

Out-of-scope findings, each carried:

The head is green (0 failure, 0 pending; the two skips by contract) and the code contract is sound on every item above; the verdict turns on ①14f alone, a published sentence this diff's own pin contradicts. Nothing is armed or readied on this head.

Implemented-by: claude/issue-15206-s3-doors-env-only
Reviewed-by: session_01Bw3y2DWhT9RPnrmDsNqEVG

VERDICT: FAIL

Claude-Session: https://claude.ai/code/session_01T33XAHecf6UARBkb45mecB
Co-authored-by: Claude <noreply@anthropic.com>

# Conflicts:
#	packages/rest/src/execctx-consumer-census.test.ts
…for the body only; a withdrawal in either layer closes the form

Claude-Session: https://claude.ai/code/session_01T33XAHecf6UARBkb45mecB
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fa9f7b6483a755ab62b3d227c9c2c6e70c944b1e
Local-runs: none

Inputs: card #15206 (body and all 32 comments, through the S3 patch-round-3 report 6080865956); PR #22447 (body, its 45-file list, the net diff against merge base 166a94f75d, which is the PR's recorded base, and its six comments: the docs-drift note, the seat's CI note 6078034955, the records 6078132696 on 9a2d880352, 6078714194 on 4a222dc938 and 6080287167 on 82bd7e85a2, and the seat's patch note 6079074423); the head's 35 check-runs, read at 2026-10-09T12:39Z. Source at the head and on origin/main was read with git show / git grep where a sentence or a consumer needed judging; objectui was read the same way at this head's pin; a git merge-tree of the head against current main was read for conflicts. Nothing was checked out, built, run or re-run.

This head against the FAIL record. Since 82bd7e85a2 the branch merged origin/main once more (e73b90754b, second parent 166a94f75d, the merge base) and added one prose commit, fa9f7b6483. Measured over the PR's 45 files, git diff --stat 82bd7e85a2 fa9f7b6483 names seven: the five copies ①14 named (the changeset, metadata-lifecycle.mdx, metadata-service.mdx, the acceptanceCriteria of 18.meta-doors-organization-scope-retired.ts, registry.ts regenerated), plus two the merge moved — rest-server.ts (main's own hunks) and execctx-consumer-census.test.ts (one hand-resolved conflict, ③). The PR's OWN hunks are byte-identical: a diff-of-diffs of rest-server.ts (old base 587bd9699d against 82bd7e85a2, new base against this head, hunk headers stripped) is empty, and the per-file diffs of domains/meta.ts, meta-write-capability.ts, meta-write-org-scope.ts, meta-item-read-gate.ts, bootstrap-declared-email-templates.ts, capabilities.ts and domains/packages.ts hash the same on both bases. So every code judgment of the three earlier records is re-made here on byte-identical code (①1–①13, condensed), and the rewritten sentence family is judged on its own against the code at this head (①14).

① Derived judgments

Gate verdicts on this head, as read. 35 check-runs: 33 success, 2 skipped, 0 failure, 0 pending. Required contexts all success: Lint & Repo Gates, TypeScript Type Check (and its four legs), Build Core, Test Core (rollup and shards 1/6 to 6/6), Dogfood Regression Gate (rollup and 3/3), Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard; check:migration-registry is a step of the Lint & Repo Gates job ("Migration registry matches its entry files", lint.yml:419), so the regenerated registry.ts is the gate's own verdict. Also success: Check Changeset, Check PR Size (1,313 + 2,698 = 4,011 changed lines, under the human-merge line), Spec property liveness, Build Docs, Dogfood Verify CLI, the docs-link and docs-flag checks, and the four claim / closing guards. Skipped by the console path filter: Console Pin Gate (the pin f0268ad784 is unchanged on this head, on 82bd7e85a2 and on main) and Packed-tarball smoke (opt-in). Not governed: no path under .claude/, docs/adr/, skills/, AGENTS.md or CLAUDE.md; head repo is the base repo; draft, auto_merge unset, mergeable: true, clean.

Accept-set and public-surface changes, re-made on this head's net diff (byte-identical code since the FAIL head):

  1. metaWriteCapabilityVerdict narrows to { isSystem, systemPermissions, operation } and admits isSystem or manage_metadata only. The manage_org_presentation arm, its declaresOrgOverride import, and the canonicalType / activeOrganizationId inputs leave; the four REST item doors (PUT, DELETE, publish, rollback) and the dispatcher's shared saveVerdict (asked by PUT, the ?state=draft author exemption and /layers) call the narrowed shape. RIGHT, and required in this same PR: with no organization threaded the arm would have admitted its holders to environment-wide authoring (stage 0's F13). One refusal sentence per verb; 403 FORBIDDEN on REST, 403 PERMISSION_DENIED on the dispatcher, protocol never reached — pinned on both transports (meta-write-capability.test.ts, meta-item-save-capability-gate, meta-write-door-capability-enumeration, runtime meta-save-capability-gate). POST /meta/_migrate-stored keeps its own manage_metadata gate.

  2. manage_org_presentation leaves PLATFORM_CAPABILITIES / PLATFORM_CAPABILITY_NAMES; ORG_PRESENTATION_AUTHORING_CAPABILITY leaves @objectstack/metadata-core. RIGHT: the arm was its only consumer; a declared capability nothing consults is the declared-but-unenforced shape this repo forbids. Residue at head, grepped over packages/*/src excluding tests and the migration entries: comments and two route-ledger notes only. The owed measurement holds (systemPermissions is a plain string list; validateCapabilityReferences adds every granted name to its known set; the seeder upserts only), which is what makes the entry a semantic/ one. Pinned in capabilities.test.ts with manage_metadata as the control.

  3. Writes carry no organization. REST PUT / DELETE / publish / rollback and the dispatcher PUT hand the protocol no organizationId (the key is absent, not undefined); organizationIdForMetaWrite is deleted, and at head the only lines naming it as an import are the two ADR-0087 entries' prescriptions and their registry.ts copies — no live importer. RIGHT; pinned at argument level for every registered type and URL spelling on both transports and at the stored row (organization_id NULL) through the real stack; the dev's reverse verification (raw tenant re-threaded at PUT, 37 of 53 red, restored blob-equal) is the recorded direction.

  4. Reads are environment → code, flipped in the same PR. REST item (both cache arms), list, layered (both spellings), /published, _drafts, /history, /diff, /audit (organizationId: null, the narrow partition inside the request type's string | null), /diagnostics (both arms), /references; dispatcher item, ?state=draft, layered, /published, list, _drafts. RIGHT. ⭐ The legacy-row pins are the load-bearing half: an organization-scoped row planted straight through the protocol is served by no /meta door, not even to its own organization, on both cache arms, beside an environment row or alone; /history, /diff, /diagnostics, /references and the dispatcher-REST parity suite likewise.

  5. Five read sites stop resolving the caller (fetchCurrentMetaDocument, which loses environmentId; the layered read; the diagnostics and references sweeps; the /published overlay read). Each removed resolveExecCtx(...).catch(rethrowAuthzStoreUnavailable) fed only the organization; no door loses a gate (/history, /diff, /audit resolve their caller at the door head behind refuseNonAuthoringCaller; the layered door's per-caller gate runs inside createMetaLayeredAnswer; /published keeps metaItemReadGate; /diagnostics and /references carried no caller gate on main beyond the organization read). RIGHT. The census rebase is re-measured at this head in ③ (the merge).

  6. /references reads environment → code though the stage plan did not name it (the dev's Q1, A). RIGHT: one door contract; the B alternative lets a legacy organization source shadow a NEW environment source of the same name before a delete.

  7. packages/runtime/src/domains/packages.ts: the ADR-0045 publish flip's organizationIdForMetaWrite('app', organizationId) call and its spread are removed. Byte-identical (app declares allowOrgOverride: false, so the call always answered undefined). RIGHT. The domain's raw organization threading stays for S4 — including assemblePackageManifest (packages.ts:2363, spent at :2397), which ①14g reads against.

  8. Only one of the three helpers is deleted. declaresOrgOverride and organizationIdForMetaRead stay in metadata-core with rewritten headers: protocol.ts imports organizationIdForMetaRead and applies it at the item, list, layered, page, history and diff reads, and the anonymous form doors' Default-Organization read (triage Q3 A) runs through them; deleting them means editing protocol.ts, fenced to S4 / S5. RIGHT.

  9. metaReadOrganizationId leaves @objectstack/rest; metaCallerOrganizationId stays (the dispatcher's /packages doors read it). RIGHT; no importer of the removed name at head. objectui re-measured at this head's pin f0268ad784 (unchanged since the earlier record): 0 files for organizationIdForMetaWrite, organizationIdForMetaRead, declaresOrgOverride, metaReadOrganizationId, metaCallerOrganizationId, ORG_PRESENTATION_AUTHORING_CAPABILITY, bootstrapEffectiveEmailTemplates and EffectiveEmailTemplateSources (control overlayScope: 75 files); 0 imports from @objectstack/metadata-core or @objectstack/rest; manage_org_presentation in 14 files, string data only. The pin gate builds and never tests, so its filter skip is the gate's contract.

  10. plugin-email's boot sweep reads environment → code: EffectiveEmailTemplateSources loses tenancy, readDeclared calls getMetaItems({ type }), email-plugin.ts no longer resolves the tenancy service. RIGHT; a legacy Default-Organization overlay is no longer projected until C7 promotes it (pinned in the unit suite and in email-template-overlay-survives-boot, whose precondition pins the org-active admin's save landing NULL).

  11. The public-form dogfood pins. An org-active manage_metadata admin's withdrawal on the walled posture (refused 403 NOT_OVERRIDABLE before) now lands environment-wide and closes both anonymous doors; the showcase twin the same. Not a widening of any principal (manage_metadata is scope: 'platform'; the same caller could already save environment-wide by clearing the active organization). ⭐ showcase-public-form-withdrawal-layers plants legacy organization overlays through the protocol and pins both directions: a legacy organization WITHDRAWAL closes a form open environment-wide, and an org-active admin's environment-wide withdrawal closes the form beneath an OPEN legacy overlay. RIGHT — and the second pin is what ①14 is judged against.

  12. Route ledgers (rest, runtime), concept.mdx, create-vs-edit-form.mdx describe the new posture; nothing published there is made false. content/docs/permissions/capabilities.mdx is rightly unchanged.

  13. Unchanged by design: protocol.ts and sys-metadata-repository.ts (S4 / S5), the anonymous form doors' read (Q3 A), the /packages doors, flow saves (control pinned on both transports), the protocol's own organization-scoped refusals, POST /meta/_migrate-stored's own gate.

  14. The rewritten published sentence family, judged clause by clause against the code at fa9f7b6483. Five copies, byte-equal in substance: .changeset/15206-meta-doors-environment-only.md:36, content/docs/concepts/metadata-lifecycle.mdx:109, content/docs/kernel/contracts/metadata-service.mdx:412-421, the acceptanceCriteria of 18.meta-doors-organization-scope-retired.ts:44-50, and its regenerated copy in registry.ts:15975-15981 (byte-equal to the entry source). The retired clauses read 0 hits at head over the tree with CHANGELOGs excluded ("body and withdrawal alike", "Public forms are the one exception", "form withdrawals, fail-closed"); "Public forms are the exception" reads exactly one hit per copy, five in all.

  • (a) "is not served by any /meta read until the promotion ceremony" and "re-save the item in Studio to make the edit live on the /meta doors now" — RIGHT, unchanged from the FAIL record's (a): scoped to the doors this diff flips; ①4 and ①3 are the pins.
  • (b) "the anonymous form doors read a form view in the Default Organization" — RIGHT: resolveFormBySlug (packages/rest/src/rest-server.ts:10342) reads type: 'view' with organizationId from anonymousFormOrganization(tenancy), which is tenancy.defaultOrgId() (:1807-1809), and undefined where no Default Organization binds the request (walled), so the sentence scopes itself by naming the organization.
  • (c) "and prefer its overlay for the form's body" — RIGHT: servedOverlayRowCandidates (packages/metadata-protocol/src/protocol.ts:2039 at this head, untouched by the PR) orders "the organization's rows, then the env-wide rows. Precedence, never a merge", and the org-scoped list the door reads carries that body; the FAIL record's (d) objection — "body and withdrawal alike" named a preference the withdrawal does not take — is answered by the clause now saying "for the form's body" and nothing more.
  • (d) "while a withdrawal in either layer closes the form, fail-closed" — RIGHT, both directions read off the code. Organization layer: a withdrawn org body yields no open candidate at all (anonymousFormIntakeCandidates requires both switches true), so the served list carries nothing to find, whatever the environment row says. Environment layer: findPublicFormView (:10293-10309) drops a candidate when layers.some(...) over anonymousFormIntakeWithdrawnIn(layer, view, c) holds, and resolveFormBySlug pushes the env-wide list into layers exactly when an organization resolved (:10361-10368); the predicate (packages/metadata-core/src/anonymous-form-intake.ts:288) matches a same-name body in that layer that explicitly withdraws the form by slot or by slug — "a withdrawal is a kill switch". Fail-closed is right as a word for both the tenancy seam (resolveAnonymousFormTenancy throws AuthzStoreUnavailableError, and the door refuses rather than falling back to an env-wide read) and the union semantics. The word is now in all five copies (the two mdx copies had dropped it). Both directions are pinned in showcase-public-form-withdrawal-layers.
  • (e) "So a legacy organization overlay of a public form keeps serving its body there: a Studio re-save of that body (an environment row) does not change the body the public form serves" — RIGHT: a Studio re-save lands organization_id NULL (①3), the env row sits behind the organization's in the served-row order (c), and the door reads the env-wide layer only into layers for the withdrawal check, never for the body. The sentence is now scoped to a re-save "of that body", which is the case that holds.
  • (f) "and a Studio withdrawal (an environment row) still closes it" — RIGHT: (d)'s environment direction, pinned in the file this PR rewrote: "withdrawn by an admin WITH an active organization: the save is environment-wide, and it closes the form beneath the open legacy overlay" (plantLegacyOrgOverlay(true) → OPEN, the org-active admin's saved(false) matches /env-wide/, probe() equals CLOSED); public-form-withdrawal-walled and showcase-public-form-withdrawal pin the environment-wide withdrawal closing both doors too. The FAIL record's ground, a universal "does not change what that public form serves" that this pin contradicted, is gone from every copy.
  • (g) "Public forms are the exception" — the FAIL record's precision flag ①14g, resolved by the first of the two folds it offered (drop "one"). Judged at head: assemblePackageManifest (packages/runtime/src/domains/packages.ts:2363, spent at :2397) still hands getMetaItems the caller's active organization, so a manifest assembled by an org-active caller still folds a legacy organization overlay; that is the /packages door, which the same changeset's "What does not change" paragraph names as untouched, and the stage plan leaves to S4. The sentence frames its exception against the /meta reads and the email sweep it follows, and no longer claims uniqueness. RIGHT as a precision matter; the package-manifest read is carried in ③.
    No sentence in the five copies states the opposite of a contract this diff pins. The S2 FAIL standard (6074635359), applied on 82bd7e85a2, is met on this head.

② Semver level

.changeset/15206-meta-doors-environment-only.md: minor on @objectstack/metadata-core, @objectstack/rest, @objectstack/runtime, @objectstack/plugin-email, @objectstack/spec — all five published (private unset at head); a title with !, the BREAKING paragraph, and a FROM → TO map covering every removed or narrowed surface (organizationIdForMetaWrite, ORG_PRESENTATION_AUTHORING_CAPABILITY, the two metaWriteCapabilityVerdict members, metaReadOrganizationId, the tenancy source, the stale permission-set grant) plus what a deployment observes, now with the ①14 sentence as judged. The level is right: Changesets is in pre mode (pre.json: mode pre, tag next), the fixed group is already majored by 22080-v18-line-opens.md (@objectstack/spec: major), S1 and S2 on this card are the precedent, and Check Changeset is success on this head. Owing nothing: @objectstack/dogfood (private, tests), @objectstack/service-datasource (one comment), metadata-protocol (untouched). ADR-0087: one marker, registered manage-org-presentation-retired, meta-doors-organization-scope-retired, in the gate's grammar registered id[, id...] (check-adr-0087-registration.mjs:62); both entries exist under migrations/entries/semantic/18.*; registry.ts is +88 lines on this head's net diff, the two entries and nothing else, the acceptanceCriteria copy byte-equal to the entry source. The entry kind is right (①2).

Clause-②: no (narrowing) on PR body line 2, in the changeset, and on claim 6076144407 — matches the diff: three exports, two verdict inputs, one interface member, one capability and one admission arm leave; nothing is added — no key, code, route, parameter, status or export (the /audit door's explicit null sits inside the request type's declared string | null).

③ Boundary flags

Dev deviations and questions (reports 6077667992, 6078632031, 6078815440, 6079504896, 6080128648, 6080865956), each answered:

  • Zone 2 "delete the three helpers" falsified → ①8. /references (Q1) → A, ①6. Five read sites no longer resolve the caller → ①5. Worktree-first and the one pre-PR --force-with-lease amend → process only, accepted as reported. The full runtime and dogfood suites NOT MEASURED locally → answered by this head's check-runs (every shard and both rollups success).
  • packages.ts minimal edit (S4 territory) → code right (①7). Still escalated to the seat: claim 6076144407's file surface does not name packages/runtime/src/domains/packages.ts; the PR body lists it under domain:cli as declared on the lanes' seat posts, which are outside this review's inputs — the seat confirms that declaration before the PR leaves draft.
  • Round 3's merge of origin/main (e73b90754b) carried ONE hand-resolved conflict, packages/rest/src/execctx-consumer-census.test.ts: main's rest: the API-description endpoints (RestServer.registerOpenApiEndpoints) answer an anonymous caller with the whole object model; ADR-0056 D2 and ADR-0138 D2 require 401 #22430 added two BARE resolveExecCtx sites (66 → 68 on its base) while this PR removed five CAUGHT ones (66 → 61 on its base); the resolution composes both — 63 sites / 78 mentions, 16 caught (12 same-line) / 47 bare, each entry kept under a merged header and marked as measured on its own base, and the structural invariant (every bare site guarded on the next line, no caught one) kept. Judged on the diff: a census rebase, not a consumer change; Test Core is success on this head, so the composed counts match the merged rest-server.ts. RIGHT.
  • Round 3's "folded 14g: dropped 'one' in all five" and "fail-closed kept in all five" → verified at head (①14 d, g).
  • The entry edit regenerated registry.ts through gen:migration-registry (406 / 247 / 222, no hand edit) → the gate's own step is success (① first paragraph), and the copy is byte-equal to the source.

New at this head, otherwise:

Out-of-scope findings, each carried:

The head is green (0 failure, 0 pending; the two skips by contract), the code contract is byte-identical to the one the three earlier records judged sound, and the one published sentence that failed 82bd7e85a2 now states the pinned contract in all five copies. Nothing blocks the seat's ACCEPT and the ready flip beyond the one declaration escalated above. A new commit on the branch, a merge of main included, is a new head and needs a new record.

Implemented-by: claude/issue-15206-s3-doors-env-only
Reviewed-by: session_01Bw3y2DWhT9RPnrmDsNqEVG

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 12:51
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 12:51
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 7895671 Oct 9, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-15206-s3-doors-env-only branch October 9, 2026 13:37
os-tesla pushed a commit that referenced this pull request Oct 9, 2026
… merging main at e148ca9 (step 18: 64 conversions, 329 semantic entries)

main added two step-18 semantic entries since 8b713fa, both from #22447
(7895671): manage-org-presentation-retired and
meta-doors-organization-scope-retired. At protocol 18 both generators
project every step-18 entry, so both documents gain them. The conversion
ids are unchanged; the 16 -> 18 aggregate is 406, equal to the registry.

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants