Skip to content

feat(metadata-protocol,objectql,rest,spec)!: every metadata read is environment → code; legacy organization rows and sealed overlays are reported at boot, not served (#15206 S5) - #22628

Merged
os-zhuang merged 23 commits into
mainfrom
claude/issue-15206-s5-reads-env-only
Oct 10, 2026
Merged

os-zhuang merged 23 commits into
mainfrom
claude/issue-15206-s5-reads-env-only

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Refs #15206 (S5)
Clause-②: no (narrowing) — every metadata read is environment → code; no read refused today is admitted.

Patch round 1 (2026-10-10T11:39Z), answering contract review 6095742860 and the docs-drift note 6094691685: main merged at d5dac9ba0 (protocol.ts merged clean; #22591's create-remedy change kept whole); spec-changes.json regenerated in 6c7c6c28d (it then carried metadata-read-organization-scope-retired); the legacy-row boot report counts the three ledgers and projects sys_metadata to the four printed columns, pinned with its ablation (82e2de41b, the cast dropped in b45b42a6c); the environment-variables.mdx merge-at-read sentence made true (62f64d223). main merged again at 40e6b5d6d because #22638 had landed and left the PR conflicted: the modify/delete on packages/spec/spec-changes.json keeps main's deletion (the file is now generated at publish only).

Stage S5 of #15206 only. #15206 remains open: S6 and the column / index / history re-keys (#15211, triage ruling Q2 B) are not in this PR.

What changes

ADR-0131 D6 retired the per-organization overlay axis. S3/S4 stopped the doors carrying an organization and made the protocol refuse every organization-scoped write. This PR makes every protocol read environment → code too:

  • Reads are env-only. The item, list, layered, cached (ETag), history, diff, audit, drafts and commit-timeline reads now serve the environment's stored row, else the code packages' definition, whatever organization the caller names. So do the _lock gate's overlay layer, search's page sweep, diagnostics, references and boot hydration. A row stored organization-scoped by an earlier release is served by no read.
    • The rule lives in one place: servedOverlayRowCandidates / findServedOverlayRow and the overlay cache key [type, packageId].
    • ItemAddress loses organizationId, and the ETag validator is locale plus content.
  • Sealed managed overlays are not served (triage ruling Q1 → C). This covers an environment row overlaying an item a managed package ships, on a type sealed against overlays: any type whose registry entry admits no overlay (a flow, action, hook, object or datasource, for example). Such a row was written through OS_METADATA_WRITABLE before the seal.
    • declinesStoredRow gains one arm, isUnservedSealedOverlay. The package's definition wins.
    • An object such row is not loaded at boot either, so the engine keeps the packaged schema.
    • Permission sets are carved out. A stored fork of a code-declared permission set keeps its 2026-08-24 ruling: detection reading plus Discard Overlay, "not a silent migration".
  • Boot reports both populations, once each, per type. Nothing is deleted or rewritten (decision: ADR-0131 C5 — under single, Studio saves are stored organization-scoped today. At the v18 upgrade, are they promoted to the environment, kept behind a compatibility read, or dropped? #22011 A).
    • [metadata_org_scoped_unserved] covers the legacy organization rows (active and draft), plus the legacy rows of sys_metadata_commit / _history / _audit. It names os migrate (ADR-0131 D10).
    • [metadata_sealed_overlay_unserved] covers the sealed overlays and names the two remedies: re-express as a linkage-free clone, or delete the row.
  • Anonymous form doors are unchanged (triage ruling Q3 A). resolveFormBySlug and findPublicFormView keep reading the Default Organization's legacy view layer, fail-closed.
    • The rest door now sends the protocol-internal legacyFormOrganizationId instead of organizationId.
    • The protocol honours that key for view alone. No spec request declares it.
    • The behaviour is pinned unchanged in rest/src/public-form-withdrawal.test.ts and in dogfood (below).
  • @objectstack/objectql binds the environment's authored hook and action rows only. Before, readAuthoredHookRows / readAuthoredActionRows bound every organization's rows.
  • Spec (narrowing).
    • organizationId leaves GetMetaItems, GetMetaItem, GetMetaItemLayered, AuditMetaItem, HistoryMetaItem and GetMetaItemCached, and the ListDraftsResponse drafts.
    • overlayScope is 'env' | null.
    • New ADR-0087 semantic entry metadata-read-organization-scope-retired.
    • The authorable-surface/api.json lines for the six vanished keys were removed by hand, following S4's route: the build and gen:authorable-surface-base both refuse a vanished key.

Carried findings

Finding Outcome
Dead organizationId spread into the authoring gate removed: MetadataAuthoringGateContext.organizationId is gone; no gate read it
Seed loader's refusal prescribed config.organizationId now prescribes organization_id on the record first (a package-published seed has no caller organization)
listCommits' organization branch report, don't serve: it lists organization_id IS NULL commits only, for every caller; legacy commits are counted in the boot report
metadata-lifecycle.mdx "Per-org overlay" wording rewritten; the D6 callout is extended; a sealed-overlay paragraph is added; metadata-service.mdx is aligned
objectql reads every organization's hook/action rows environment rows only (a row-value check, isLegacyOrganizationRow: a where-clause spelling trips the federated-readers gate)
declaresOrgOverride / organizationIdForMetaRead callers none left; deleted (metadata-core/src/meta-write-org-scope.ts and its test)
Read callers outside the claim passing organizationId plugin-security permission-set-projection.ts / permission-set-overlay-discard.ts (dropped); the rest form doors (now the legacy key); the rest audit door (organizationId: null dropped); runtime domains/packages.ts (comments only)

Cross-lane paths

  • domain:cli: packages/rest/src/execctx-consumer-census.test.ts, packages/rest/src/meta-item-layered-route.test.ts, packages/rest/src/meta-object-extension-property-classes.test.ts, packages/rest/src/meta-object-overlay-extension-fold.test.ts, packages/rest/src/public-form-withdrawal.test.ts, packages/rest/src/rest-server-audit-org-scope.test.ts, packages/rest/src/rest-server-meta-cached-etag-door-scope.test.ts, packages/rest/src/rest-server.ts, packages/runtime/src/audit-meta-item-org-scope.integration.test.ts, packages/runtime/src/domains/domain-protocol-handle-typing.test.ts, packages/runtime/src/domains/meta-read-org-scope-parity.test.ts, packages/runtime/src/domains/packages-protocol-handle-typing.test.ts, packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts, packages/runtime/src/domains/packages.ts, packages/runtime/src/meta-write-org-scope.test.ts, packages/runtime/src/package-list-commits-org-scope.integration.test.ts, packages/runtime/src/package-revert-stored-members.integration.test.ts
  • domain:devx: content/docs/concepts/metadata-lifecycle.mdx, content/docs/kernel/contracts/metadata-service.mdx, content/docs/references/api/protocol.mdx, packages/qa/dogfood/test/showcase-object-extension-scalar-divergence.dogfood.test.ts, scripts/engine-double-contract.pinned.json, scripts/objectql-double-limit.baseline.json
  • domain:services: packages/plugins/plugin-security/src/permission-set-overlay-discard.ts, packages/plugins/plugin-security/src/permission-set-projection.test.ts, packages/plugins/plugin-security/src/permission-set-projection.ts
  • domain:spec: packages/spec/authorable-surface/api.json, packages/spec/src/api/meta-item-response-shapes.test.ts, packages/spec/src/api/protocol.test.ts, packages/spec/src/api/protocol.zod.ts, packages/spec/src/migrations/entries/semantic/18.metadata-read-organization-scope-retired.ts, packages/spec/src/migrations/registry.ts

Census (Q1 → C: census first)

Measured on the two seeded example apps, booted fresh with their seeds by a throwaway @objectstack/verify probe at HEAD. The probe was not committed. It counted every row of the four ledger tables through the engine:

App sys_metadata rows legacy organization rows env rows carrying a package _commit / _history / _audit legacy rows
CRM (@objectstack/example-crm) 0 0 0 0 / 0 / 0
Showcase (examples/app-showcase) 0 0 0 0 / 0 / 0

What this shows. A freshly seeded deployment carries neither population. Nothing is dropped, and nothing in a fresh install changes what it serves.

What it does not show. Real deployment databases are not reachable from this container, so their counts are NOT MEASURED. The boot report this PR adds is the census instrument for them. It prints both populations per type on the first boot of this build, before anyone acts on a row. No row is touched, which honours "nothing is dropped by guess" and #22011 A.

Both reports fired correctly in the dogfood run, on fixtures that plant the rows:

  • [metadata_org_scoped_unserved] … flow×1 (showcase_task_assigned_notify@org_dogfood_21002)
  • [metadata_sealed_overlay_unserved] … object×1 (showcase_account), flow×1 (showcase_urgent_task_alert), datasource×2 (showcase_external, default)

Reverse verification

Both ablations ran from committed state 2ccf45701 through scripts/ablation-replace.mjs (WRAP mode, anchor hit ×1 and verified on disk). The subject is metadata-protocol/src, imported from source by the pin, so no dist leg applies.

# Mutation (protocol.ts) Expected Observed
1 drop organization_id: candidate.organizationId from findServedOverlayRow's where (blob 5c3dc429 → 647a3a8a) §1 red 10 failed / 10 passed: every §1 case (5 tier-A types × 2 caller shapes)
2 drop || this.isUnservedSealedOverlay(type, name) from declinesStoredRow (blob 5c3dc429 → 47977733) §2 red 2 failed / 18 passed: the managed action and managed hook cases
restore git checkout HEAD by absolute path green blob == HEAD 5c3dc429, git diff HEAD empty, ABLATION- marker count 0; pin 20 passed

The direction was the usual one: red under each mutation, green after restore.

Tests

The package suites ran on 4560ed20, which carries the final source. The later commits touched no runtime source: the new pin's fake engine (2ccf4570; pin rerun 20 passed), one dogfood case (ea26adb03; rerun 7/7 passed), and the ADR-0087 entry / changeset / docs wording of the seal's reach (5616042ea). For that commit, gen:migration-registry was rerun, spec check:generated --fix reported all 15 artifacts up to date, and spec's src/migrations + scripts tests passed 1,733. Each heavy run went through scripts/pm/os-verify-lock.sh.

Package Command Result
@objectstack/metadata-core pnpm --filter … test 16 files, 384 passed
@objectstack/metadata-protocol pnpm --filter … test 219 files (3 skipped), 28,029 passed / 19 skipped
@objectstack/objectql pnpm --filter … test 392 files, 7,729 passed
@objectstack/rest pnpm --filter … test 268 files, 4,973 passed / 326 skipped
@objectstack/runtime vitest --shard=1/2, 2/2 2,979 + 2,680 passed
@objectstack/plugin-security vitest (full) 192 files, 4,033 passed (see note 1)
@objectstack/spec vitest --shard=1/3, 2/3, 3/3 7,146 + 6,092 + 6,630 passed
new pin protocol.reads-environment-only.test.ts vitest 20 passed
typecheck the seven packages above, plus @objectstack/dogfood all exit 0
dogfood (17 files that read metadata layers) vitest 100 passed after one case was re-premised (see note 2)

Note 1 — plugin-security first run. It failed 9 files, each with "Cannot find package '@objectstack/spec/…'", while a concurrent gate rebuilt spec's dist. No test body ran. The rerun on the same tree was green.

Note 2 — dogfood. The 17 files are:

  • flow-shipped-name-* (×4), flow-unshipped-name-layered-code
  • managed-content-sealed, email-template-overlay-survives-boot, datasource-* (×2)
  • permission-set-discard-overlay-eligibility
  • showcase-public-form-withdrawal, showcase-public-form-withdrawal-layers, public-form-withdrawal-walled
  • showcase-object-extension-scalar-divergence, external-validate-sees-runtime-save, package-first-authoring, object-designer-field-reorder

The first run had 1 failed / 99 passed: showcase-object-extension-scalar-divergence › "SHOULD: a tenant's own rename reaches the reads its forms derive from". That case pinned a pre-seal environment rename of the packaged object showcase_account being served, which is exactly what triage ruling Q1 → C stops. It is re-premised, not weakened: the row is still at rest and shown by ?layers=true, the item and list reads serve the package's label, and boot names the row. The file then ran 7/7 passed.

Q3 A holds. The three public-form withdrawal dogfood files are green.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 134 families at 5616042ea, the same set as one commit earlier. All 134 ran on that tree, each exit code captured before any pipe.

Reconciliation: --ran prints 134 derived, 134 run, 0 NOT-MEASURED, 0 UNRUN. The zero is derived from the recorded codes: every family exited 0.

Repaired along the way:

  • check:engine-double-contract: the ledger is rewritten with --write. Two pins left with the deleted files, and the new pin's three doubles were added.
  • check:objectql-double-limit: two baseline entries removed for files that are gone or now clean. The new pin's find double now applies limit.
  • check:where-matcher: the new pin's matcher now refuses any combinator or operator it does not implement.

Gates that refused at first:

  • check-engine-split-ratio --days 90 and check-plugin-teardown-shape --self-test refused on the shallow clone. After deepening they measured, and passed.
  • check:skill-examples, check:dual-build-cjs-loads and check:i18n refused at first because packages had no dist. Once the dogfood closure was built they measured, and passed.

Notes the gates print themselves, with exit 0:

  • check-spec-docblock-symbol-anchors --self-test: "second reality pin NOT TAKEN" (the objectui sha is not in this clone).
  • check:entry-nameability: "NOT MEASURED: no callable export on @objectstack/spec/api-assembled, @objectstack/spec/qa". It prints this regardless of this diff.

Repo-wide lint (pnpm lint) was not run locally; that run belongs to CI.

Size route: human merge

The diff is 6,188 changed lines across 71 files, over HUMAN_MERGE_LINE_THRESHOLD = 3000 (scripts/pm/check-governed-merges.mjs), so this PR takes the human route. About 70% of the size is deletion: whole test files whose subject S5 retires (five organization-read-gate and ETag-scope pins, and the meta-write-org-scope helper and its test), plus the organization arms of re-premised pins. Breakdown: 1,768 added + 4,420 deleted = 6,188 at 40e6b5d6d; source +615/−1,249, tests +1,023/−3,114, docs+changeset +66/−25, generated/ledgers +64/−32.

objectui at the pinned .objectui-sha (report only; carrier C9)

At 47b1f0bb71:

  • Live behaviour. LayeredDiff.tsx renders translateConsoleValue('layer', layered.overlayScope ?? 'set'). After S5 the server never emits 'org', so the org label is dead but harmless.
  • Type break on the next spec bump. packages/data-objectstack/src/metadata-client.ts:374 derives MetadataOverlayScope from GetMetaItemLayeredResponse['overlayScope']. packages/app-shell/src/views/metadata-admin/i18n.ts:7151 declares LAYER_SCOPE_ZH as a Record keyed by the non-null MetadataOverlayScope values, with an org key. Once objectui moves to this spec, that literal's org key is an excess property (a typecheck error).
  • Tests to update with that bump. Several test fixtures spell overlayScope: 'org': metadata-client.layeredEnvelope/layeredRoute, ResourceEditPage.resetVerdict and StudioDesignSurface.listViewInspector-11823.

No objectui edit here.

Acceptance notes

维护者速读(草稿)

改了什么:元数据的所有读取口(单项、列表、分层、ETag 缓存、历史、差异、审计、草稿、提交时间线、锁、搜索、引用、启动加载)一律只读「环境 → 代码」两层,不再读任何组织级旧行。旧版本存下的组织级行、以及在封存前通过 OS_METADATA_WRITABLE 写下的「托管包内容的覆盖行」(flow/action/hook/object/datasource 等不允许覆盖的类型),都不再被使用,启动时按类型各报一次,并给出处理办法。公开表单的匿名入口照旧读默认组织的旧 view 行(裁决 Q3 A)。

为什么改:ADR-0131 D6 已退役按组织覆盖元数据的能力,写入口在 S3/S4 已全部拒绝组织级写入;如果读取口还继续提供旧的组织行,退役的那一层就会按调用方逐个"复活",不同组织看到不同的元数据。托管包内容已封存,读取也必须让包定义胜出。

风险与代价(含回滚):依赖旧组织级覆盖行的部署,升级后这些定制不再生效,回到环境行或包定义(启动日志会逐条点名,数据一行未删,可在 v18 迁移仪式中提升,或在 Studio 重新保存为环境级)。依赖封存前覆盖托管内容的部署,同样回到包定义,需改为另起名的克隆。权限集的存储分叉不受影响。回滚:回退本 PR 即恢复原读取行为,无数据迁移需要撤销。objectui 下次升级 spec 时需删掉 overlayScope 的 org 键(一处类型 + 若干测试)。

席位意见:

你要做的:本 PR 超过 3000 行门槛,走人工合并:请审阅后人工批准合并。重点看:权限集是否应被排除在"封存覆盖不提供"之外;封存 object 覆盖行不再在启动时叠加到引擎这一行为变化是否可接受。


Generated by Claude Code

claude added 15 commits October 10, 2026 01:33
…egacy organization rows

Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
Co-authored-by: Claude <noreply@anthropic.com>
…own combinators; ledgers follow the deleted pins (#15206)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
…s no overlay, not four named ones (#15206)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 10, 2026
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

45 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 ee3ae0360dc5beff655e4feb8f9624c731e306fd.

⛔ 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/spec/authorable-surface/api.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: organization_id (literal, 33 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 — 148 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 ee3ae0360dc5beff655e4feb8f9624c731e306fd → packageMentionDocs.

Which tree this was computed on

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

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

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

…t rest, not served (#15206)

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

Copy link
Copy Markdown
Contributor Author

Contract review

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

Inputs: card #15206 (body and all 45 comments; bound to the stage plan 6067844889, stage 0's S5 row 6067752061, triage's Q2 B / Q3 A 6068032120 and Q1 → C 6073941543, S4's review 6090242635 and the S5 claim 6092019285; the S5 os-dev-report 6094700038); #22011 (body and its ruling A, 6020163868); the 2026-08-24 permission-set ruling as packaged-permission-set-lock.ts, packaged-permission-set-overlay-detection.ts and ADR-0126 record it on main; ADR-0131 D6, D7, D10 and ADR-0087 (D2 and the disposition addenda), read on main at 86da194919; PR #22628 (body, its 71-file list, its one comment — the docs-drift note — and the net diff against its merge base: 71 files, +1,708 / −4,419, equal to the file list); the 35 check-runs on the head, all completed. Source on main read for the predicates the diff leans on (isSealedManagedItem, registryAllowsOverlay, isArtifactShippedHook / isArtifactShippedAction, the two projection gates' --check, the regeneration registry and .gitattributes). Nothing was checked out, built, run or re-run.

① Derived judgments

Gate verdicts on this head, as read. Every required context is success: Lint & Repo Gates (the leg carrying check:adr-0087-registration, check:migration-registry, check:engine-double-contract, check:objectql-double-limit and check:where-matcher), TypeScript Type Check and its four legs (· source gates runs check:authorable-surface, check:spec-changes, check:upgrade-guide and check:docs), Test Core and all six shards, Dogfood Regression Gate (rollup and 1/3 to 3/3), Build Core, Temporal Conformance (live PG + MySQL) and Governed Surface Queue Guard. Also green: Build Docs, Dogfood Verify CLI, Check Changeset, Check PR Size, Spec property liveness, the docs-link and docs-flag checks and the four claim and closing guards; Console Pin Gate and the packed-tarball smoke skipped by contract. The combined status is success (one external context). Not governed: no path under .claude/, docs/adr/, skills/, docs/NORTH-STAR.md, AGENTS.md or CLAUDE.md; head repo is the base repo; draft, auto_merge unset, read mergeable by GitHub. Size: 6,127 changed lines over 71 files — over HUMAN_MERGE_LINE_THRESHOLD (3000 on main) and over the 5,000 of AGENTS.md §7(c) — the human-merge route the body names; the route is the seat's.

Every read that stops serving a row, each read off the net diff and judged.

  1. The served-row resolution is one rule, environment only. servedOverlayRowCandidates loses its organization scope (legacyOrganizationId is the one optional arm, below); findServedOverlayRow reads organization_id IS NULL rows only and no longer returns a scope; getOverlayRepo() takes no argument and is the environment singleton, with legacyOrganizationRepo(id) kept for exactly one caller, S4's uninstall removal (removeLegacyOrganizationRowOnUninstall, unchanged in effect). RIGHT: the stage plan's S5 row, D6 ("environment → code"), D7. The 20-case pin protocol.reads-environment-only.test.ts §1 drives five tier-A types × two caller shapes through item, list, layered, history, diff, audit, drafts, commits, lock and ETag; the dev's reverse verification (10 red with the organization_id predicate dropped, restored blob-equal) is the recorded direction, and Test Core (3/6) ran it green.
  2. Item, list, layered, cached. getMetaItem / getMetaItemLayered / getMetaItemCached / getMetaItems lose organizationId; readFlattenedMetaItems' two-scope union and its draft-preview arm become one environment read (queryDrafts(null, …) only); the overlay cache key is [type, packageId]; the layered overlay layer is the environment row and overlayScope is 'env' or null; the ETag validator is locale plus content, so one validator per item for every caller. RIGHT. The S4-carried residue — organizationId spread into the authoring gate, MetadataAuthoringGateContext.organizationId, assertRuntimeAuthoringRules' organization input and foldStoredCollection's second scope — is deleted, not re-plumbed.
  3. History, diff, audit, drafts, commits. historyMetaItem and diffMetaItem read the environment lineage; auditMetaItem builds organization_id: null unconditionally (the auditMetaItem's organizationId is dead on both ends — the audit read returns every org's rows for a (type, name), while its comment describes a scope filter that is not in the query #8747 scope is kept, narrowed); listDrafts and SysMetadataRepository.listDrafts list organization_id IS NULL drafts and drop the per-draft organizationId; packageScopedRowWhere loses its organization parameter (not exported from the entry); listCommits is { package_id, organization_id: null } for every caller, so rollbackToPackageCommit's plan can no longer contain a legacy organization commit. RIGHT, and the trade S4 ①5 recorded is closed the consistent way: the whole-timeline read that showed an organization-active operator every organization's legacy commits is gone, and the legacy commits are counted in the boot report instead (item 7). revertCommit of a legacy commit stays 403 by S4's gate, as the dev notes.
  4. The _lock gate, search, diagnostics, references, version tokens, credential carry-forward. getEffectiveLock / readLockGateOverlayLayer / overlayLockLayerAt / resolveOverlayLockLayer read the environment rows (ITEM_ADDRESS_FIELDS and ItemAddress lose organizationId, StoredOverlayRowReader loses its scope parameter — all internal to the package, not entry exports); searchAll's page sweep, getMetaDiagnostics, findReferencesToMeta, readVersionToken, restoredCredentialPathsFor, collectBatchPendingDeclarations and publishPackageDrafts' pre-publish active-row read are environment reads. RIGHT.
  5. The new decline arm. declinesStoredRow gains isUnservedSealedOverlay(type, name) = isSealedManagedItem minus permission. On main, isSealedManagedItem is isArtifactBacked(type, name) && !registryAllowsOverlay(type), and registryAllowsOverlay is the allowOrgOverride set — the five regime-O types. So the arm declines an environment row over ANY item a code package ships whose type is not one of the five: the flow, action, hook, object and datasource the body names, and equally a managed page, app, dataset or book (the A legacy env overlay on an artifact-backed item of a rolled-back type can no longer be REMOVED through the ordinary delete path (403) — only via OS_METADATA_WRITABLE #6960 merge-at-read class that stage 0 noted as still merging over a managed artifact; the re-premised protocol-registry-shadow pin now serves the packaged app's label over the stored row). RIGHT, on two grounds: Q1 → C defines the population as "overlay rows on sealed items", and the dev takes the seal's own predicate so the write refusal and the read narrowing cannot disagree; and D6 recognises no regime for such a row. The reach is wider than the body's examples and is stated here so nobody reads "flow, action, hook, object" as the whole class. The permission carve-out is judged in ③ Q1. The three objectql binders are not a gap: isArtifactShippedHook / isArtifactShippedAction already skip an authored row whose name a code package ships (artifact-wins), so the package definition wins at execution as well as at read. Pinned: §2 of the new pin (managed action and hook declined; the authored control served; a regime-O view overlay served; a permission fork served), and the dev's second ablation (2 red with the arm dropped).
  6. Boot hydration. loadMetaFromDb keeps organization_id IS NULL; an object row that is a sealed overlay is not loaded (it would register as the layer ADR-0029 D9 makes the engine resolve the object through), and the read-side hydration in readFlattenedMetaItems skips the same rows; every other sealed type still hydrates its bare copy, which the reads decline (the automation: at kernel:ready the flow sync re-arms a stored row's body over the loader's for a packaged flow name, after the boot pull armed the loader's, so the stored body runs while the receipt says the package's is armed #20913 shape, unchanged). RIGHT — judged in ③ Q2. The re-premised dogfood case (showcase-object-extension-scalar-divergence) pins exactly this: the pre-seal rename stays at rest and shows under ?layers=true, item and list serve the package's label, boot names the row; Dogfood Regression Gate is green.
  7. The two boot reports. [metadata_org_scoped_unserved] replaces [metadata_org_scoped_unhydrated] (the old token survives only in release-owned CHANGELOGs): type-blind, active and draft rows, the JS re-check of organization_id, a capped sample per type, plus a count of the legacy rows of sys_metadata_commit / _history / _audit, naming os migrate and that nothing is deleted or rewritten; [metadata_sealed_overlay_unserved] lists the sealed overlays per type with the two remedies Q1 → C names (a linkage-free clone, or delete the row). Both at warn: a functional degradation an operator can see (the customised behaviour is gone, the package's or the environment's is served), nothing claimed persisted is lost — the right level under AGENTS.md's one question. Both inside the diagnostic's own catch, so neither can become the outage (pinned: the probe-failure case). RIGHT, with one carried note: the three ledger counts are finds with no limit whose rows are loaded only to be counted; on a single deployment every pre-S3 Studio save left an organization-scoped audit row, so this is an unbounded read at every boot — a count, or a measured page, is the shape (③). Pinned in the re-premised protocol.org-scoped-cold-boot-audit suite and §3 of the new pin.
  8. objectql. readAuthoredHookRows, readAuthoredActionRows and the object-rows arm skip a row with a non-empty organization_id (a row-value check, isLegacyOrganizationRow, as the federated-readers gate requires). RIGHT: stage 0's carried finding, the binders now follow the reads' rule; the legacy rows are in the type-blind boot report.
  9. The form doors (Q3 A). resolveFormBySlug and findPublicFormView keep reading the Default Organization's legacy view layer: the rest door sends legacyFormOrganizationId in place of organizationId; the protocol honours it in readFlattenedMetaItems for canonicalMetaType(type) === 'view' alone (readActiveOverlayRows merges that organization's rows over the environment's, uncached, the organization's row winning its slot), and findServedOverlayRow never sees it, so the by-name read is untouched. The doors' object read becomes the environment's. No spec request declares the key, and no public door forwards a raw request object into getMetaItems — the runtime dispatcher and every REST door build typed literals — so the key is reachable in-process only. RIGHT; judged as a spelling in ③ Q3. Pinned on both sides: public-form-withdrawal.test.ts (every read's organizationId undefined, the two view reads [ORG, undefined] per door, FORM_NOT_FOUND and fail-closed cases unchanged) and §4 of the new pin (the key reaches no other type); the three withdrawal dogfood files are green.
  10. plugin-security and runtime. projectPermissionMutation's layered read names no organization (it reads what every reader reads), permission-set-overlay-discard.ts and the write-through drop the key; domains/packages.ts changes comments only. RIGHT.
  11. Spec (narrowing). organizationId leaves GetMetaItems, GetMetaItem, GetMetaItemLayered, AuditMetaItem, HistoryMetaItem and GetMetaItemCached requests and each ListDraftsResponse draft; overlayScope is z.enum(['env']).nullable(). None of the request schemas is .strict(), so a request literal still carrying the key is stripped at a spec parse and reads the environment; the response schema now refuses 'org' — which no server emits after this head. authorable-surface/api.json loses the six lines by hand (the vanished-key route S4 took; Type Check · source gates and Spec property liveness are green), protocol.mdx is regenerated with the schemas (check:docs green), and the GetMetaItemResponse.version and lock descriptions no longer name an organization partition. RIGHT. objectui at the pin: the dev reports the LAYER_SCOPE_ZH org key and four fixtures that will break on the next spec bump (carrier C9); whether objectui reads drafts[].organizationId is NOT MEASURED here (no objectui input in this review) — the bump owner reads it.
  12. metadata-core. meta-write-org-scope.ts and its test are deleted and the entry export removed: declaresOrgOverride and organizationIdForMetaRead had no caller left (TypeScript Type Check green). RIGHT; a published removal, in the changeset.
  13. Published text against the code. The changeset's "What changes" and FROM → TO rows each hold against the diff; the ADR-0087 entry's surface / replacement / acceptanceCriteria match the pin and the reports; metadata-lifecycle.mdx's D6 callout now says the protocol refuses and every read is environment → code (S4's carried sentence), with a new sealed-overlay paragraph that names the permission exception; metadata-service.mdx is aligned; the OS_METADATA_WRITABLE row of environment-variables.mdx turns "still served" into "kept at rest but not served" with the boot token and the remedies. One sentence of that row is left imprecise by item 5: "A row of a type that merges overlays at read (permission, position, page, app, dataset, book, tool, skill) can still be removed" — the removal door (mergesOverlayAtRead, read by deleteMetaItem alone) is intact, but after this head only permission still merges such a row at read; the other seven are declined. The operative claim holds; the characterisation is carried (③).

Nothing is deleted or rewritten. No added line in the diff calls a store write or delete (insert, update, delete, put, restoreVersion, promoteDraft); the one organization-scoped write left in the class is S4's uninstall removal, which this diff only renames the accessor of. The sys_metadata family's objects and overlay-index.ts are not in the file list.

② Semver level

.changeset/15206-reads-environment-only.md: minor on @objectstack/metadata-protocol, @objectstack/metadata-core, @objectstack/objectql, @objectstack/rest, @objectstack/plugin-security and @objectstack/spec — all six published, each with a behaviour or type narrowing in the diff; a title with !; a BREAKING (narrowing) paragraph; a FROM → TO table covering the six spec request keys, the five protocol request keys, the drafts member, overlayScope: 'org', the two metadata-core exports, MetadataAuthoringGateContext.organizationId, the legacy organization row and the sealed overlay. The level is right: Changesets is in pre mode with tag next, the fixed group is already majored by the line's opening marker, S1 to S4 on this card are the precedent, and Check Changeset is green. Owing nothing: @objectstack/runtime (comments only), @objectstack/dogfood (private), @objectstack/cli (tests only). Two published type narrowings the table does not name — projectPermissionMutation's evt parameter (exported from plugin-security's entry) and SysMetadataRepository.listDrafts' row shape (the class is exported; the drafts row covers it in substance) — are inside packages the changeset already bumps with the BREAKING arm; the gate judges the claim made, so this is a note for the seat, not a ground.

ADR-0087: one marker, registered metadata-read-organization-scope-retired, in the gate's grammar; the entry exists under migrations/entries/semantic/18.* and registry.ts on the net diff is that one entry, byte-equal to its source (check:adr-0087-registration, check:migration-registry are Lint & Repo Gates steps, green). The kind is right: semantic/, because no authorable key is tombstoned (the six vanished keys take the no-tombstone route S4 took) and the read's old answer has no lossless mapping. The committed projection packages/spec/spec-changes.json at this head does not carry the new id (S4's two ids: 2 hits each; this one: 0). That is not a gate miss to argue with: check:spec-changes generates in memory and compares no committed copy by design (the publish lane generates the shipped one and verifies it from the tarball), so check:generated --fix's "15/15 up to date" cannot see it. The committed copy is a merge=os-regen artifact, so one gen:spec-changes on the tree, committed alone, is a pure regeneration that stays under this record (③). docs/protocol-upgrade-guide.md is not owed: it left the generated set at #22483 and nothing regenerates it (its full-guide shape on main is a pre-existing inconsistency, noted below); content/docs/protocol-upgrade/** is gitignored build output.

Clause-②: no (narrowing) on PR body line 2, in the changeset and on the claim 6092019285 — matches the diff. no: no read that is refused today is admitted — every change above removes a row from an answer, a key from a request, a value from an enum, a binding from a boot; the one new key, legacyFormOrganizationId, reproduces for view alone the reach organizationId already gave the two form doors, in-process only. (narrowing): the BREAKING arm names what leaves. Right.

③ Boundary flags

Dev deviations and open questions, from the S5 report 6094700038, each answered:

  • deviations. Two post-push corrections (the seal's reach worded as every non-overlay type, not four named ones; the registry regenerated with it) and one dogfood case re-premised under Q1 → C: process, accepted; the re-premised case is item ①6's evidence, not a weakened pin.
  • Q1 (permission carve-out) → A, confirmed. The 2026-08-24 maintainer ruling on existing permission-set forks is specific and standing: a detection reading plus the operator's Discard Overlay, "no automatic reap/merge … not a silent migration". Declining a stored fork at read would change the grants a deployment enforces with no operator act — the effect the ruling refused — and triage's Q1 → C named flow, action, hook and object, not permission sets. The carve-out is the one place the read narrowing and the seal's write refusal differ, and that asymmetry is the ruling's own shape (lock the base; existing forks reported, discarded by hand). The body's 维护者速读 puts the question in front of the maintainer for the human merge, which is the right place for a reversal if one comes.
  • Q2 (a sealed object overlay is not loaded at boot; other sealed types hydrate a bare copy) → A, confirmed. An object row is the one type whose hydration changes the engine's effective schema (ADR-0029 D9's layer), so not loading it is what makes "the package definition wins" true for the engine, not only for /meta; every other type's bare copy is declined at read by the existing automation: at kernel:ready the flow sync re-arms a stored row's body over the loader's for a packaged flow name, after the boot pull armed the loader's, so the stored body runs while the receipt says the package's is armed #20913 shape, and the dev measured B (skip every sealed type) reddening the flow-shipped-name pins the bare copy carries. ADR-0029 D9 is not reversed in general — ADR-0131 D6, the later accepted record, seals the managed object the overlay would layer over; the dated note on ADR-0029 D9 that triage's Q1 → C assigned to S7 (Tier H) is still owed there. A registry-direct getItem on a bare copy of a non-object sealed type is the pre-existing automation: at kernel:ready the flow sync re-arms a stored row's body over the loader's for a packaged flow name, after the boot pull armed the loader's, so the stored body runs while the receipt says the package's is armed #20913 exposure, not new.
  • Q3 (legacyFormOrganizationId as a protocol-internal key) → A, confirmed. The only callers are the two anonymous form doors; the read retires with C7's carry (Q3 A, decision: ADR-0131 C5 — 17.x honours a public form's withdrawal saved at the organization layer. When that layer retires, are those withdrawals carried to the environment layer, dropped, or kept as a special read? #22008 A); declaring the key in @objectstack/spec would publish a request member for a read that is scheduled to leave; no public door forwards a raw request object, and the pin shows the key reaches no other type.
  • Carried from S4 ③, each closed here: the authoring-gate spread and MetadataAuthoringGateContext.organizationId (deleted); the seed loader's config.organizationId prescription (now organization_id on the record first, the publish path named); listCommits' organization branch and the door's whole-timeline read (environment only, legacy commits counted at boot); metadata-lifecycle.mdx's D6 callout (says the protocol refuses and the reads are environment → code); Zone 2 (getOverlayRepo is one environment repository; the repository's organizationId option stays for the uninstall removal alone; stored-migration.ts keeps organizationId on the migration row only to describe the row S4's skipped report names — not a read, accepted); S4 ①8's two uninstall residue classes are the population the type-blind boot report now names (their fate is C7's); the runtime suites whose names still say org-scope now pin environment-wide behaviour (churn, as S4 said). From S3's review: the per-organization overlay wording is rewritten. From stage 0: the hook and action binders.
  • The claim's ⛔ Not list holds: the anonymous form doors' organization-layer withdrawal read is kept (①9); no stored row is deleted or rewritten (①, last paragraph); no column or index work on the sys_metadata family (no such file in the diff).
  • Undeclared cross-lane paths. The report's cross_lane_paths list (rest, runtime, plugin-security, spec, dogfood, docs and the two script ledgers) is longer than the claim's dispatch-time declaration (it adds the dogfood case, scripts/engine-double-contract.pinned.json and scripts/objectql-double-limit.baseline.json, and the runtime and rest test files by name); whether each is on its lane post before the PR leaves draft is outside this review's inputs — escalated to the seat, as in S4.

For the seat before the landing (none a verdict ground):

Verdict grounds, all on this head: every required context is green; every read enumerated in the stage plan's S5 row stops serving a legacy organization row and the sealed overlays, with the one form-door exception Q3 A keeps and the one permission exception the 2026-08-24 ruling keeps; both boot reports fire, at the right level, and delete nothing; the spec narrowings, the ADR-0087 entry and the changeset's level and arm are judged sound above; the carried S4 items are closed; the claim's ⛔ Not list holds. What remains is a pure regeneration of one committed projection, declarations and follow-ups — the seat's steps, not defects in the diff.

Implemented-by: claude/issue-15206-s5-reads-env-only
Reviewed-by: session_01Bw3y2DWhT9RPnrmDsNqEVG

VERDICT: PASS

… type, not an erasure (#15206)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lgdatg1AaT6mAeCQaQJZgR
…-reads-env-only

# Conflicts:
#	packages/spec/spec-changes.json
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 40e6b5d6d75908b295545c97f414f9affc117088
Local-runs: none

Inputs: card #15206 (body and all 48 comments; bound to the stage plan 6067844889, stage 0's S5 row 6067752061, triage's Q2 B / Q3 A 6068032120 and Q1 → C 6073941543, S4's review 6090242635, the S5 claim 6092019285, the first S5 os-dev-report 6094700038 and the patch round 1 report 6097121217; also the seat's status note 6095810656 and the domain:spec pointer 6095970259); #22011 (body and its ruling A, 6020163868); the 2026-08-24 permission-set ruling as packaged-permission-set-lock.ts, packaged-permission-set-overlay-detection.ts and ADR-0126 record it on main; ADR-0131 D6, D7, D10 (and the D14 execution-plan note of #22619) and ADR-0087 D2, D4 as amended by #22449 B′ and the two disposition addenda, read on main at 243dd3c625; the earlier record 6095742860 (PASS on bb6012af86), re-judged against this head rather than carried; PR #22628 (body, its 71-file list, its two comments — the docs-drift note and that record — and the net diff against its merge base ee3ae0360d: 71 files, +1,768 / −4,420, equal to the file list); the delta bb6012af86..40e6b5d6d7 (54 commits: 46 of main's, the two merges, five of the dev's); the 41 check-runs on the head (two runs of the PR-automation workflow) and the one commit status. Source at the head was read through the contents API — protocol.ts whole, packaged-base-regime.ts, the package entries of metadata-protocol and plugin-security, rest-server.ts, objectql's plugin.ts and engine.ts (count), runtime's domains/meta.ts, spec's data-engine.ts, meta-item-read-gate.ts, the new pin — and on main: ci.yml, pr-automation.yml, check-governed-merges.mjs, position-write-through.ts. For the first merge the four protocol.ts blobs (base 6a3f82efa7, both parents, the merge) were fetched and the merge was compared against a three-way text merge of the parents. Nothing was checked out, built, run or re-run.

① Derived judgments

Gate verdicts on this head, as read. Every required context is success: Lint & Repo Gates (its steps include "Migration registry matches its entry files", "Engine query-options erasure ratchet", "Error-code provenance guard" and ESLint), TypeScript Type Check and its four legs (· source gates ran "Check spec-changes.json generates from the ADR-0087 registries", "Check the protocol upgrade guide generates from the ADR-0087 registries", "Check the authorable key surface is recorded and nothing vanished" and "Check generated reference docs are in sync with the spec"), Test Core and all six shards, Dogfood Regression Gate (rollup and 1/3 to 3/3), Build Core, Temporal Conformance (live PG + MySQL) and Governed Surface Queue Guard. Also green: Check Changeset (whose step "Require an ADR-0087 disposition on a declared-breaking changeset" ran check-adr-0087-registration.mjs --base on the merge base), Build Docs, Dogfood Verify CLI, Check PR Size, Spec property liveness, the docs-link and docs-flag checks and the four claim and closing guards (twice each). Skipped by contract: Console Pin Gate (the filter saw no console path; the pin is unchanged on this head and on main), Packed-tarball smoke (opt-in), and the second run's Auto Label / Check PR Size twins. One commit status is failure: Vercel (deployment dpl_CoYU7V4AQCuGXfkKZVUanmETd5eM, set after this head's commit). It is an external status, not a check-run and not one of the ruleset's seven required contexts, and Build Docs — the command production runs — is green on the same head; it is not a gate verdict and not a ground, and it goes to the seat (③). Not governed: no path under .claude/, docs/adr/, skills/, docs/NORTH-STAR.md, AGENTS.md or CLAUDE.md; head repo is the base repo; draft, auto_merge unset, GitHub reads it mergeable (unstable is the Vercel status). Size: 6,188 changed lines over 71 files — over HUMAN_MERGE_LINE_THRESHOLD (3000 on main) and over the 5,000 of AGENTS.md §7(c) — the human-merge route the body names; the route is the seat's.

Why 6095742860 does not carry, and what the delta did. Merge 1 (d5dac9ba0, parents bb6012af86 and cc305a3cfc): three PR-touched files moved on both sides — protocol.ts (#22623: packagedBaseRefusal gains operation: 'create'), content/docs/references/api/protocol.mdx and packages/spec/src/migrations/registry.ts. The protocol.ts at d5dac9ba0 is byte-identical to a three-way text merge of the two parents over their base, with zero conflict regions: the clean automatic merge the dev reported, every #22623 line present, S5's own hunks intact. The other two are regenerated artifacts, and the head's "Check generated reference docs are in sync with the spec" and "Migration registry matches its entry files" steps are green — what a regeneration has to satisfy. Merge 2 (40e6b5d6d7, parents b45b42a6c2 and ee3ae0360d): protocol.ts at the head is byte-identical to the branch parent and main did not touch it in that span; the only overlaps were registry.ts (regenerated, green) and the modify/delete on packages/spec/spec-changes.json, resolved to main's deletion (#22638) — the file is absent from the net diff, as main is. The merge base against main is ee3ae0360d and the net diff against it is exactly the 71-file list. The dev's five commits between: the protocol.mdx regeneration (b8ad3c549), the spec-changes.json regeneration (6c7c6c28d, moot after merge 2), the boot-report count and projection with its two pins (82e2de41b: protocol.ts +26/−14, the new pin +44/−7, the cold-boot suite +10), the environment-variables.mdx sentence (62f64d223), and the one-line cast drop (b45b42a6c). Nothing else moved on the branch side. Every item below was judged on the whole net diff at this head.

Every read that stops serving a row, each read off the net diff and judged.

  1. The served-row resolution is one rule, environment only. servedOverlayRowCandidates takes legacyOrganizationId as its one optional arm (item 9) and otherwise yields the environment scope alone; findServedOverlayRow reads organization_id: null rows only and returns no scope; getOverlayRepo() takes no argument and is the environment singleton; legacyOrganizationRepo(id) exists for exactly one caller, S4's uninstall removal (removeLegacyOrganizationRowOnUninstall), and the test seeders. RIGHT: the stage plan's S5 row, D6 ("environment → code"), D7. The 20-case pin protocol.reads-environment-only.test.ts §1 drives the five tier-A types × two caller shapes through item, list, layered, history, diff, audit, drafts, commits, lock and ETag; the dev's reverse verification (10 red with the organization_id predicate dropped, restored blob-equal) is the recorded direction; Test Core (3/6) is green.
  2. Item, list, layered, cached. getMetaItem (both the draft-preview and active arms), getMetaItemLayered, getMetaItemCached, getMetaItems and getMetaItemsForExecution lose organizationId; readFlattenedMetaItems' two-scope union and its draft-preview arm become one environment read (queryDrafts(null, …) only, the stand-in drafts too); readActiveOverlayRows reads queryByOrg(null) and the overlay cache key is [type, packageId]; the layered overlay layer is the environment row and overlayScope is 'env' or null (the rowless-expansion arm too); the ETag validator is locale plus content, so one validator per item for every caller. RIGHT. The S4-carried residue — MetadataAuthoringGateContext.organizationId, runAuthoringGate's spread, assertRuntimeAuthoringRules' organization input and foldStoredCollection's second scope — is deleted, not re-plumbed.
  3. History, diff, audit, drafts, commits, version token. historyMetaItem and diffMetaItem read the environment lineage (org: 'env', orgId = null); auditMetaItem builds organization_id: null unconditionally (the auditMetaItem's organizationId is dead on both ends — the audit read returns every org's rows for a (type, name), while its comment describes a scope filter that is not in the query #8747 scope kept, narrowed); listDrafts and SysMetadataRepository.listDrafts list through packageScopedRowWhere(state, filter), which is { state, organization_id: null } plus the filter, and the per-draft organizationId is gone; collectBatchPendingDeclarations reads the environment repository; publishPackageDrafts' pre-publish active-row read is organization_id: null; listCommits is { package_id, organization_id: null } for every caller, so rollbackToPackageCommit's plan can no longer contain a legacy organization commit; readVersionToken and restoredCredentialPathsFor read org: 'env'. RIGHT, and S4 ①5's trade (an org-active operator shown every organization's legacy commits) is closed the consistent way — counted at boot (item 7). revertCommit of a legacy commit stays 403 by S4's gate.
  4. The _lock gate, search, diagnostics, references. getEffectiveLock / readLockGateOverlayLayer / overlayLockLayerAt read organization_id: null; ITEM_ADDRESS_FIELDS and ItemAddress lose organizationId, StoredOverlayRowReader loses its scope parameter and resolveOverlayLockLayer its scope loop — all internal to the package (item-lock.ts is not an entry export); searchAll's page sweep is getMetaItems({ type: 'page' }); getMetaDiagnostics and findReferencesToMeta lose organizationId; resolveRowlessExpandedView reads the environment's containers. RIGHT.
  5. The decline arm and its reach. declinesStoredRow gains isUnservedSealedOverlay(type, name) = isSealedManagedItem minus permission; on this head isSealedManagedItem is isArtifactBacked(type, name) && !registryAllowsOverlay(type), and registryAllowsOverlay is the allowOrgOverride set, the five regime-O types. So the arm declines an environment row over ANY item a code package ships whose type is not one of the five: the flow, action, hook, object and datasource the body names, and equally the supportsOverlay merge-at-read class — position, page, app, dataset, book, tool, skill (OVERLAY_CAPABLE_TYPES, A legacy env overlay on an artifact-backed item of a rolled-back type can no longer be REMOVED through the ordinary delete path (403) — only via OS_METADATA_WRITABLE #6960) — which the environment-variables.mdx row now states by name; the re-premised protocol-registry-shadow pin shows the packaged app's label served over the stored row. RIGHT, on two grounds: Q1 → C defines the population as "overlay rows on sealed items" and names the overlay regime as covering only the five presentational types, and the dev takes the seal's own predicate so the write refusal (refusePackagedBaseOverride, saveMetaItem) and the read narrowing cannot disagree. On position: main's C2 S10 module (position-write-through.ts) already states that position takes no environment overlay and that the metadata door refuses a save over a packaged name, so such a row can only pre-date the seal — in no regime, declined, consistent with D3. A stated limit, not a defect: for a sealed row of a type other than object, loadMetaFromDb still registers the bare, tenant-marked copy (hydrateOverlayIntoRegistry, _provenance: 'org'); every protocol read declines it (isStoredEntryOfDeclinedName) and getArtifactItem is immune, but a registry-direct getItem(type, name) reader, if one exists, would still meet that shadow — the automation: at kernel:ready the flow sync re-arms a stored row's body over the loader's for a packaged flow name, after the boot pull armed the loader's, so the stored body runs while the receipt says the package's is armed #20913 shape this PR does not change, and the dev's Q2 → A (③). The published sentences "no read serves it" describe the protocol reads, which is what those pages document. The three objectql binders are not a gap: isArtifactShippedHook / isArtifactShippedAction already skip an authored row whose name a code package ships. Pinned: §2 of the new pin (managed action and hook declined; the authored control served; a regime-O view overlay served; a permission fork served), the protocol-object-overlay-layer suite re-premised (the sealed object row reported, never layered), and the dev's second ablation (2 red with the arm dropped).
  6. Boot hydration. loadMetaFromDb keeps { state: 'active', organization_id: null }; a sealed object row is collected for the report and skipped before convertStoredItem (it would register as the layer ADR-0029 D9 makes the engine resolve the object through); the read-side hydration in readFlattenedMetaItems skips the same rows (request.type === 'object' and the arm); every other sealed type still hydrates its bare copy, which the reads decline. RIGHT — judged in ③ Q2. The re-premised dogfood case (showcase-object-extension-scalar-divergence) pins it end to end: the pre-seal rename stays at rest and shows under ?layers=true, the item and list reads serve the package's label; Dogfood Regression Gate is green.
  7. The two boot reports, with the count and projection reads added since 6095742860. [metadata_org_scoped_unserved] replaces [metadata_org_scoped_unhydrated] (the old token survives only in release-owned CHANGELOGs): type-blind, active and draft rows, a capped sample per type, and the three ledger counts, naming os migrate (D10) and that nothing is deleted or rewritten; [metadata_sealed_overlay_unserved] lists the sealed overlays per type with Q1 → C's two remedies (a linkage-free clone, or delete the row); both at warn (a functional degradation an operator can see, nothing claimed persisted is lost); the sealed report is synchronous and runs before the legacy one, and both are inside the diagnostic's own catch so neither can become the outage (pinned: the probe-failure case). The new reads: the one sys_metadata find carries where: { state: { $in: ['active','draft'] }, $and: [{ organization_id: { $null: false } }, { organization_id: { $ne: '' } }] } and fields: ['type', 'name', 'organization_id', 'state'], with the JS re-check kept on organization_id for a driver that hands back a superset; each ledger is engine.count(ledger, { where: LEGACY, context: { isSystem: true } }) through orNoTable (a missing table reads as 0 or empty; any other failure stays in the diagnostic's catch). RIGHT: $null, $ne and $and are in the spec's filter vocabulary (filter-comparand-operators.ts) and the drivers lower them; IDataEngine.count at this head admits exactly context and where as query keys (ENGINE_COUNT_OPTION_KEYS), merges query.context with the third argument's (mergeReadContext) and rides the AST through the same middleware as find, so the ADR-0096 system opt-in holds on the count as on the find — the shape the protocol's own search path already uses on engine.count — and a driver without count falls back to find(...).length on fields: ['id']: bounded either way, and no ledger body is read. The cast drop (b45b42a6c) is what the erasure ratchet asked for ("Engine query-options erasure ratchet" green). Pinned: §3's second case (no find on any ledger; exactly one legacy sys_metadata find whose fields equal the four columns; the '' audit row not counted; the line's text) and the cold-boot suite's new ledger case; the dev's 3a/3b ablations (the first 3a run voided and rerun, as reported) are the recorded direction. What is NOT MEASURED on this head: no test asserts either token against a real engine — the dogfood observation in the PR body predates 82e2de41b, and no dogfood file names the tokens — so a count that threw for a non-table reason would silence the legacy line without reddening a gate. The contract read above says it does not throw; a one-boot reading on the showcase, or one dogfood assertion, is the seat's cheap confirmation (③), not a ground.
  8. objectql. readAuthoredHookRows, readAuthoredActionRows and the object-rows arm skip a row with a non-empty organization_id (isLegacyOrganizationRow, a row-value check — the federated-readers gate's shape, as the dev says). RIGHT: stage 0's carried finding; the binders follow the reads' rule and the rows are in the type-blind report.
  9. The form doors (Q3 A). resolveFormBySlug and findPublicFormView keep reading the Default Organization's legacy view layer: the rest door's viewsRequest is a LegacyFormViewsRequest literal carrying legacyFormOrganizationId in place of organizationId; the protocol honours it in readFlattenedMetaItems for canonicalMetaType(type) === 'view' alone (readActiveOverlayRows merges that organization's rows over the environment's, outside the cache, the organization's row winning its slot), findServedOverlayRow never sees it, so the by-name read is untouched; the doors' object reads are the environment's. No spec request declares the key, and no door forwards a raw request: the rest server's one getMetaItems(request) with a variable is the book-tree door's own TransportScopedMetaRequest literal, and the runtime dispatcher (domains/meta.ts) builds typed literals on every getMetaItem / getMetaItems / getMetaItemLayered call — so the key is reachable in-process only. RIGHT; judged as a spelling in ③ Q3. Pinned on both sides: public-form-withdrawal.test.ts (every read's organizationId undefined, the view reads [ORG, undefined] per door, FORM_NOT_FOUND and fail-closed unchanged), §4 of the new pin (the key reaches no other type), the re-premised write-refused and view-container suites; the three withdrawal dogfood files are green.
  10. plugin-security and runtime. projectPermissionMutation's layered read names no organization, permission-set-overlay-discard.ts and the write-through drop the key; domains/packages.ts changes comments only. RIGHT.
  11. Spec (narrowing). organizationId leaves GetMetaItems, GetMetaItem, GetMetaItemLayered, AuditMetaItem, HistoryMetaItem and GetMetaItemCached requests and each ListDraftsResponse draft; overlayScope is z.enum(['env']).nullable(); the version and lock descriptions name no organization partition. None of the request schemas is .strict(), so a request literal still carrying the key is stripped at a spec parse and reads the environment — the spec pins are rewritten to that contract (stripped-at-parse per schema, 'organizationId' in schema.shape false, a @ts-expect-error on the audit literal, 'org' refused on the response). authorable-surface/api.json loses the six lines by hand (the vanished-key route S4 took; the "authorable key surface" and Spec property liveness steps are green), protocol.mdx is the regenerated reference. meta-item-read-gate.ts passes overlayScope through untouched, and no other emitter of 'org' was found at this head. RIGHT. objectui at the pin (47b1f0bb71, unchanged by this PR): the dev reports LAYER_SCOPE_ZH's org key and four fixtures that break on the next spec bump (carrier C9); the drafts[].organizationId reader, if any, is NOT MEASURED here (no objectui input) — the bump owner reads it.
  12. metadata-core. meta-write-org-scope.ts exported exactly declaresOrgOverride and organizationIdForMetaRead; both are deleted with the module and the entry export (no caller left; TypeScript Type Check green). RIGHT; the published removal is in the changeset's table.
  13. Published text against the code at this head. The changeset's "What changes" and FROM → TO rows each hold against the diff; the ADR-0087 entry's surface / replacement / acceptanceCriteria match the pin and the two reports; metadata-lifecycle.mdx's D6 callout says the protocol refuses and every read is environment → code, with the sealed-overlay paragraph naming the permission exception; metadata-service.mdx is aligned; the OS_METADATA_WRITABLE row of environment-variables.mdx is the sentence 6095742860 ①13 asked for — a stored row over a managed item of the other merge-at-read types "can still be removed, but removing it changes nothing that is served", which is what refusePackagedBaseRemoval's intact carve-out and the arm make true, with permission as the one row whose removal restores the package at read. The seed loader's refusal now prescribes organization_id on the record first, naming the package-publish path. RIGHT.

Nothing is deleted or rewritten. No added line in the net diff calls a store write or delete (insert, update, delete, put, restoreVersion, promoteDraft); the one organization-scoped write in the class is S4's uninstall removal, of which this diff renames the accessor only. The sys_metadata family's objects and overlay-index.ts are not in the file list.

② Semver level

.changeset/15206-reads-environment-only.md: minor on @objectstack/metadata-protocol, @objectstack/metadata-core, @objectstack/objectql, @objectstack/rest, @objectstack/plugin-security and @objectstack/spec — all six published, each with a behaviour or type narrowing in the diff; a title with !; a BREAKING (narrowing) paragraph; a FROM → TO table covering the six spec request keys, the five protocol request keys, the drafts member, overlayScope: 'org', the two metadata-core exports, MetadataAuthoringGateContext.organizationId (an entry export), the legacy organization row and the sealed overlay. The level is right: Changesets is in pre mode with tag next, the fixed group is already majored by the line's opening marker, S1 to S4 on this card are the precedent, and Check Changeset is green. Owing nothing: @objectstack/runtime (comments only), @objectstack/dogfood (private), and the scripts ledgers. Two published type narrowings the table still does not name — projectPermissionMutation's evt parameter (an entry export of plugin-security) and SysMetadataRepository.listDrafts' row shape (the drafts row covers it in substance) — sit inside packages the changeset already bumps with the BREAKING arm; a completeness note for the seat, not a ground.

ADR-0087: one marker, registered metadata-read-organization-scope-retired, in the gate's grammar; the entry exists under migrations/entries/semantic/18.*, and registry.ts on the net diff is that one entry, byte-equal to its source ("Migration registry matches its entry files" green). The kind is right: semantic/, because no authorable key is tombstoned and the read's old answer has no lossless mapping. The registration is judged on the basis main now has: packages/spec/spec-changes.json is no longer committed (#22638, ADR-0087 D4 as amended by #22449 B′ — both projections are generated at publish), so no committed projection is owed and the head carries none; the pull-request stage still generates both in memory — "Check spec-changes.json generates from the ADR-0087 registries" and "Check the protocol upgrade guide generates from the ADR-0087 registries" are green, with the rendered projection diff uploaded — and check-adr-0087-registration.mjs --base ran on the merge base in Check Changeset, green, which is the gate that reads the marker's arm and verifies the id is new in the diff. docs/protocol-upgrade-guide.md is the pointer stub on main (#22556) and is not in the diff; content/docs/protocol-upgrade/** is build output.

Clause-②: no (narrowing) on PR body line 2, in the changeset and on the claim 6092019285 — matches the diff. no: no read that is refused today is admitted — every change above removes a row from an answer, a key from a request, a value from an enum, a binding from a boot; the one new key, legacyFormOrganizationId, reproduces for view alone the reach organizationId already gave the two form doors, in-process only. (narrowing): the BREAKING arm names what leaves. Right.

③ Boundary flags

Every item 6095742860 raised for the seat, answered on this head:

Dev deviations, open questions and out-of-scope findings, both reports, each answered:

  • 6094700038 deviations (the seal's reach re-worded as every non-overlay type with the registry regenerated; one dogfood case re-premised) — process, accepted; the re-premised case is ①6's evidence, not a weakened pin.
  • Q1 (permission carve-out) → A, confirmed on this head. The 2026-08-24 ruling on existing permission-set forks is specific and standing, as packaged-permission-set-overlay-detection.ts records it: a detection reading plus the operator's Discard Overlay, "no automatic reap/merge … not a silent migration". Declining a stored fork at read would change the grants a deployment enforces with no operator act — the effect the ruling refused — and Q1 → C named flow, action, hook and object. The carve-out is the one place the read narrowing and the seal's write refusal differ, and that asymmetry is the ruling's own shape. The body's quick-read puts it in front of the maintainer for the human merge, the right place for a reversal if one comes.
  • Q2 (a sealed object overlay is not loaded at boot; other sealed types hydrate a bare copy) → A, confirmed. An object row is the one type whose hydration changes the engine's effective schema (ADR-0029 D9's layer), so not loading it is what makes "the package definition wins" true for the engine and not only for /meta; every other type's bare copy is declined by the existing automation: at kernel:ready the flow sync re-arms a stored row's body over the loader's for a packaged flow name, after the boot pull armed the loader's, so the stored body runs while the receipt says the package's is armed #20913 shape at every protocol read (①5's stated limit is that shape, not a new exposure), and the dev measured B reddening the flow-shipped-name pins the bare copy carries. ADR-0029 D9 is not reversed in general; the dated note on it that Q1 → C assigned to S7 (Tier H) is still owed there.
  • Q3 (legacyFormOrganizationId as a protocol-internal key) → A, confirmed. The only callers are the two anonymous form doors; the read retires with C7's carry (Q3 A, decision: ADR-0131 C5 — 17.x honours a public form's withdrawal saved at the organization layer. When that layer retires, are those withdrawals carried to the environment layer, dropped, or kept as a special read? #22008 A); declaring the key in @objectstack/spec would publish a request member for a read scheduled to leave; no door forwards a raw request (①9), and the pin shows the key reaches no other type.
  • 6097121217 deviations — (a) the second merge of main: needed, spec(changes): delete the committed spec-changes.json and its merge=os-regen route; both projections are gitignored and generated at publish only (#22449 B′, card ③) #22638 had left the PR conflicted; accepted. (b) the void first 3a ablation, rerun with a valid mutation: recorded, accepted. (c) the PR body written twice through the relay: accepted.
  • 6097121217 out-of-scope findings — (1) refusePackagedBaseRemoval's TSDoc still says removing a merge-at-read row "restores the managed definition" for every such type; after this head that holds for permission only. A code-comment imprecision; fix on the next touch of that method or in a comments-only follow-up; not a ground. (2) carrier "the S5 contract reviewer": reportSealedOverlayRows' TSDoc names "a managed flow, action, hook or object" while the printed line is type-generic — accepted as the same class; declinesStoredRow's TSDoc carries the same four-name wording, and the three go together on that next touch.
  • Carried from S4 ③ and stage 0 — the authoring-gate spread and MetadataAuthoringGateContext.organizationId (deleted); the seed loader's prescription (fixed); listCommits' organization branch and the door's whole-timeline read (environment only, counted at boot); metadata-lifecycle.mdx's callout (rewritten); Zone 2 (getOverlayRepo() is one environment repository; the repository's organizationId option stays for the uninstall removal alone); the hook and action binders — all closed on this head.
  • The claim's ⛔ Not list holds: the anonymous form doors' organization-layer withdrawal read is kept (①9); no stored row is deleted or rewritten (①, last paragraph); no column or index work on the sys_metadata family (no such file in the diff).

For the seat before the landing (none a verdict ground):

Verdict grounds, all on this head: every required context is green; the two merges are the clean three-way merge and the ruled deletion, with every regenerated artifact green on its gate; every read enumerated in the stage plan's S5 row stops serving a legacy organization row and the sealed overlays, with the one form-door exception Q3 A keeps and the one permission exception the 2026-08-24 ruling keeps; both boot reports fire, at the right level, bounded, and delete nothing; the spec narrowings, the ADR-0087 registration on the no-committed-projection basis, and the changeset's level and arm are judged sound above; every item 6095742860 raised is answered or overtaken; the claim's ⛔ Not list holds. What remains is an external status to read, one cheap confirmation, declarations and follow-ups — the seat's steps, not defects in the diff.

Implemented-by: claude/issue-15206-s5-reads-env-only
Reviewed-by: session_01Bw3y2DWhT9RPnrmDsNqEVG

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

domain:engine#2 · session_01Bw3y2DWhT9RPnrmDsNqEVG · 2026-10-10T13:04Z · 复核记录是本 PR 的 6097501912(PASS,head 40e6b5d6d)。ACCEPT 在 #15206 的 6097795222。

改了什么:ADR-0131 C5 的 S5 阶段。这是 C5 最后一个代码阶段,元数据从此只剩「环境 → 代码」两层。

  • 元数据协议的每一个读操作都只读环境这一层,读不到再退回代码包的定义,不再看调用方带的组织。这包括单项、列表、分层、带 ETag 的缓存读、历史、diff、审计、草稿、提交时间线、锁、搜索、诊断和引用。
  • 旧版本按组织存下的行,任何读都不再返回,启动时也不加载。启动日志里会列出这些行:每种类型几条、点名前几条,再加三张台账表的计数,并指向 v18 迁移仪式(os migrate)。
  • 按 triage 裁决 Q1 → C:有人用 OS_METADATA_WRITABLE 后门,对托管包的、本不该被覆盖的条目(flow、action、hook、object 等)写过环境覆盖行,这些行不再生效,生效的是包里的定义。启动日志单独列出它们,并给出两种补救办法。权限集的 fork 按 2026-08-24 的裁决例外,照旧生效。
  • spec 的六个读请求去掉了 organizationId;ListDraftsResponse 的条目去掉了 organizationId;overlayScope 去掉了 'org'。ADR-0087 条目是 metadata-read-organization-scope-retired。
  • 匿名表单入口按 Q3 A,暂时保留读默认组织的旧 view 层,改走一个协议内部键。等迁移仪式时再删。

为什么改:ADR-0131 D6 规定环境元数据属于整个部署。S3、S4 已经让写入拒绝组织,读侧却还在按组织合并。S5 让读写一致,组织层从此彻底退场。

风险与代价(含回滚):

  • 这是破坏性收窄。 依赖旧组织行生效的部署,会看到这些定制「消失」,改为显示环境行或包定义。数据没有丢,行原样留在库里。补救是在 Studio 里重新保存一次,把它变成环境级;或者等 v18 迁移仪式自动提升。启动日志会逐类点名。
  • 用后门改过托管 flow、action、hook、object 的部署,那些改动不再生效。启动日志列出了补救办法:另起新名字的条目,或者删掉这条覆盖行。
  • 影响面。 objectui 读 overlayScope: 'org' 和草稿的 organizationId 的地方,在合入时转给 objectui(C9)。cloud 按裁决冻结在 v17,不受影响。
  • 规模与拆分。 6,188 行,删的多(主要是针对已删除行为的测试)。不拆:读侧必须一起收窄,否则同一条目从不同入口读会得到不同答案。
  • 已实测:启动日志那一行在真实驱动上是对的(6097632007,只读,没有推送)。 在 SQLite 上埋了旧组织行,再起 showcase:日志点名了埋入的正式行和草稿行,三张台账各计 1 条,organization_id 为空串的行没有计入。什么都不埋时不打印。已知局限:如果某张台账表本身结构损坏、计数报错,这一整行会消失,只剩驱动自己的报错日志。本 PR 不处理,留到后续阶段加固。
  • 未实测:Vercel 文档预览构建失败。 它不是必需检查;CI 里的 Build Docs 在这个 head 上是绿的;main 上同一构建一直成功。平时的 PR 都会被忽略脚本跳过,最近 40 个 PR 里只有这个 head 真正构建了。读日志需要 Vercel 凭据,本席没有。你有权限的话,执行 npx vercel inspect dpl_CoYU7V4AQCuGXfkKZVUanmETd5eM --logs,就能确认失败原因是基础设施问题还是本 PR 改坏了某个页面。
  • 回滚。 revert 这一个 PR 即可。没有改写或删除任何存储行,所以不用修数据。

席位意见:建议批准。

  • 第一次复核在 bb6012af86 上 PASS。随后补了一轮:合并 main 两次,把启动诊断改成计数加 4 列投影,修正一句文档。新 head 重新复核,再次 PASS,必需检查全绿。启动日志那一行又在真实驱动上实测过一次。
  • 开发者提的三个问题都按已有裁决判 A,复核两次确认,没有新增产品决定。
  • 批准后由本席盯合并队列。如果 main 又动了生成文件,导致队列失败,就合一次 main、重新生成,再请复核续记录。

你要做的:用 os-zhuang 或 hotlong 对本 PR 提交一次 Approve,或者亲手合入。⛔ 本席不会转 ready、不入队、不开 auto-merge。

@os-zhuang
os-zhuang marked this pull request as ready for review October 10, 2026 13:09
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit c8b062f Oct 10, 2026
47 of 48 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-15206-s5-reads-env-only branch October 10, 2026 14:03
This was referenced Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants