Skip to content

fix(metadata-protocol)!: one reader of OS_METADATA_WRITABLE — the legacy OBJECTSTACK_METADATA_WRITABLE, removed in 11.0, no longer opens the hatch at the type listing - #22440

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22411-hatch-legacy-spelling
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22411-hatch-legacy-spelling

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22411

Clause-②: no (narrowing)

What

OS_METADATA_WRITABLE now has one reader. ObjectStackProtocolImplementation.envWritableTypes() feeds GET /api/v1/meta/types and the protocol's write gates (isOverlayAllowed: the code-only create gate and the org-scope gate). It now delegates to the repository's envWritableMetadataTypes(), which reads OS_METADATA_WRITABLE only. resetEnvWritableCache() and resetEnvWritableMetadataTypes() both clear that reader's single cache. Under the legacy spelling alone, the listing and the save door now agree, because both refuse.

Why

The 11.0 release removed ObjectStack's own legacy env names as a published breaking change. That was fdb41c0 (PR #2383), with the v11-remove-env-aliases changeset in the 11.1.0 CHANGELOG of @objectstack/cli, @objectstack/objectql and @objectstack/types, and docs/upgrading-to-11.md:121. The change edited the repository's reader and missed the protocol's hand-copied one, which kept honouring OBJECTSTACK_METADATA_WRITABLE.

Measured on origin/main e02833c at the dispatcher /meta door, on both kernel shapes, with only OBJECTSTACK_METADATA_WRITABLE=job set:

  • GET /meta/types listed job with allowOrgOverride: true and overrideSource: env;
  • PUT /meta/job/NEW answered 403 NOT_CREATABLE and stored no row.

The seat's ruling on the card (comment 6076328784) is option B: finish 11.0's removal at the reader it missed. That ruling supersedes the card's earlier "do not retire the alias on this card" line. Premise re-checked on b9222dc70 before building: git grep OBJECTSTACK_METADATA_WRITABLE finds no non-test producer. The non-test hits are prose (docs, ADR-0010, the S2 changeset) plus the reader itself. The positive control, OS_AUTH_SECRET over docker/, examples/ and apps/, finds 5 hits.

Changes

  • packages/metadata-protocol/src/sys-metadata-repository.ts: envWritableMetadataTypes is exported at module level and its TSDoc names it the one reader. The package index does not re-export it, so the public surface is unchanged.
  • packages/metadata-protocol/src/protocol.ts: envWritableTypes delegates to it, resetEnvWritableCache clears the shared cache, and the now-unused readEnvWithDeprecation import is removed.
  • S2's dual-spelling pins keep their preferred-spelling legs. Their legacy legs flip to "the hatch is shut", and each keeps the preferred spelling as its control:
    • protocol.packaged-base-refusal.test.ts: the listing reports flow and page as allowOrgOverride: false with overrideSource: registry, and every managed-item verdict equals the shut one.
    • packages/rest rest-meta-managed-seal-hatch.test.ts: the same listing reading via GET /api/v1/meta/types, and every PUT (and DELETE, on an environment kernel) answers what it answers with nothing set.
    • packages/runtime meta-managed-content-seal.test.ts: the card's pins at the dispatcher door. Under the legacy spelling alone, both job (the type named) and picklist (a type not named) are listed shut and saved 403 NOT_CREATABLE with no row. Control, OS_METADATA_WRITABLE=job: job is listed env and saved 200 with a row, and picklist is still refused.
  • content/docs/deployment/environment-variables.mdx: the alias row goes back into the "Removed in 11" note, which undoes the move made in PR docs: implementation-accuracy pass — retire deprecated titleFormat example + evidence-backed doc fixes #2640.
  • .changeset/22411-hatch-legacy-spelling.md: BREAKING on @objectstack/metadata-protocol, graded minor on the v18 pre line. It names the FROM and TO spellings and the one-line operator fix, and says it finishes 11.0's removal at the reader that kept it. ADR-0087 disposition: not-required (no-migration-prescription). check:adr-0087-registration exits 0.

Verification (head adbb0a5)

  • pnpm --filter @objectstack/metadata-protocol test: 223 files passed, 3 skipped; 28330 tests passed, 19 skipped. Lock VERDICT command-exit 0.
  • Typecheck: pnpm --filter @objectstack/metadata-protocol run typecheck exits 0. The rest and runtime run typecheck runs also exit 0, and their check:test-typecheck passes over each package's tsconfig.test.json.
  • The three edited pin files: runtime 10/10, rest 7/7, metadata-protocol 31/31.
  • Every other test file in the downstream packages that touches the hatch passes:
    • objectql: 8 files, 176 tests;
    • plugin-security: 3 files, 40 tests;
    • rest: 4 files, 36 tests;
    • runtime: 3 files, 38 tests.
      Those suites read @objectstack/metadata-protocol through dist/, which was rebuilt from this branch first.
  • Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derives 94 commands. All were run, plus the 4 roster gates whose roster sits under a changed directory (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:route-ledger-census). --ran reports 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN. Two gates first answered PREREQUISITE NOT MET (exit 3) for unbuilt dists: check:skill-examples and check:dual-build-cjs-loads. Both were re-run after the builds at the same head, and both exit 0.
  • Lint: the narrowing is proven, not assumed. eslint --no-inline-config --format json over the 5 changed TS files reports 5 files, 0 errors and 0 warnings. eslint's own config ignores the .md and .mdx ("File ignored because no matching configuration"). The config enables no type-aware linting: no parserOptions.project or projectService, and 0 typed rules in --print-config for protocol.ts. So this diff cannot move the verdict on any untouched file.
  • Not measured locally, owned by CI: the dogfood boots (managed-content-sealed.dogfood.test.ts mentions the legacy spelling only in a comment) and the full rest and runtime suites.

Reverse verification of the one reader

scripts/ablation-replace.mjs put the protocol's second, legacy-honouring reader back inside envWritableTypes. On disk: anchor 1 to 0, blob 0cf53ab1 to ef809f30.

  • src, no build: protocol.packaged-base-refusal.test.ts gives 2 failed and 29 passed. The 2 failures are exactly the legacy-shut cases, on both kernels.
  • dist, rebuilt: after rebuilding @objectstack/metadata-protocol, ablation-dist-preflight finds the marker in dist/index.js and dist/index.cjs.
    • runtime: 2 failed, 8 passed. The failures are the card's legacy-alone case on both kernels; under the mutation the listing answered allowOrgOverride: true / env again.
    • rest: 2 failed, 5 passed. The failures are the legacy-shut cases.
  • Restore: the blob equals HEAD and git diff HEAD is empty. After the rebuild, preflight --absent passes on all 24 built files with a clean tree. The green legs come back at 31/31, 10/10 and 7/7.

The result went the expected way: the pins turned red.

Acceptance notes

  • docs/adr/0010-metadata-protection-model.md (:42, :301, :540, :652) still describes OBJECTSTACK_METADATA_WRITABLE as the live variable. That file is Tier H and pre-11 history, and per the ruling it stays with the seat, outside this code PR.
  • The changeset states the operator action as a sentence naming FROM, TO and the fix, not as a FROM / TO table row. With a table row, check:adr-0087-registration refuses not-required (no-migration-prescription) (evidence: header-framed-table). Its closed vocabulary has no category for a deployment-environment rename:
    • registered would put a prescription in the ledger that os migrate meta cannot project;
    • unpublished and already-registered do not apply;
    • dropping BREAKING is forbidden.
      The marker says this in writing, following the precedent of plugin-auth's OS_PLATFORM_OWNER_EMAIL changeset. Who takes up that vocabulary gap: nobody yet.
  • Under the legacy spelling alone, the save door's refusal now comes from the protocol's code-only gate, not the repository's. Code and status are unchanged (403 NOT_CREATABLE). Nothing reads the legacy name any more, so its deprecation warning no longer prints.
  • Left as written because they are still true: the S2 changeset's "(and its legacy spelling …) used to open one", the dogfood header comment, and the sys-metadata-repository.package-writability.test.ts legacy case comment ("this reader honours only OS_METADATA_WRITABLE").

Generated by Claude Code

claude added 5 commits October 9, 2026 07:19
…tocol stops honouring the spelling 11.0 removed

The 11.0 removal of ObjectStack's own legacy env names edited the
repository's reader of OS_METADATA_WRITABLE and missed the protocol's
hand-copied one, which kept honouring OBJECTSTACK_METADATA_WRITABLE. Under
the legacy spelling alone the type listing advertised the hatch for a type
while a create of a new item of that type answered 403 NOT_CREATABLE.

The protocol's envWritableTypes() now delegates to the repository's
envWritableMetadataTypes(), the one reader, which reads only the preferred
spelling; the two reset hooks clear its one cache.

Claude-Session: https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG
Co-authored-by: Claude <noreply@anthropic.com>
…hut — listing and save door agree

S2's dual-spelling seal pins keep their preferred-spelling legs; the legacy
legs now assert the hatch is shut under OBJECTSTACK_METADATA_WRITABLE alone
(listing reports no env override, every refusal equals the shut one), with
the preferred spelling as the control. The dispatcher pin covers the card's
two readings: under the legacy spelling alone the type listing and the save
door both refuse, for a type the hatch would name (job) and one it would not
(picklist).

Claude-Session: https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG
Co-authored-by: Claude <noreply@anthropic.com>
…11"; breaking changeset for the one reader

Claude-Session: https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG
Co-authored-by: Claude <noreply@anthropic.com>
…-environment rename

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

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 5 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 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 — 11 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 da159f74e60afdd1f9cfb87ce6405381354932e0 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 da159f74e60afdd1f9cfb87ce6405381354932e0 → 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

Lint & Repo Gates is red on adbb0a55, at the step "ADR symbol anchors resolve". This is this PR's failure. domain:engine#2 · session_01Bw3y2DWhT9RPnrmDsNqEVG · 2026-10-09T08:21Z.

  • The assertion, reproduced on this head with node scripts/check-adr-symbol-anchors.mjs (exit 1): [unresolved-symbol] docs/adr/0029-kernel-object-ownership-and-platform-objects-decomposition.md:605 packages/metadata-protocol/src/protocol.ts#OS_METADATA_WRITABLE.
  • The cause: this PR moves the only reader of the setting into sys-metadata-repository.ts, so protocol.ts no longer holds the literal the ADR anchors.
  • The fix is the anchor, not the code. ⛔ No literal is put back into protocol.ts to satisfy it. The ADR names the operator hatch's reader, and the reader now lives in the repository. On origin/main that file already holds the literal, so a path-only correction resolves both before and after this PR.
  • Route: docs/adr/** is Tier H, so the correction goes in its own draft PR (Refs #22411), which lands with the maintainer's approval. This PR then merges main and stays outside the governed surface. Until then this PR waits on that one gate. Every other check-run on this head is reported separately at ACCEPT.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 09:40
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 09:40
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 3ca71b6 Oct 9, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22411-hatch-legacy-spelling branch October 9, 2026 10:24
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/m tests tooling

Projects

None yet

2 participants