Skip to content

fix(metadata-protocol)!: an object_extension item's refusals fit the item door, and its target is the key the object is registered under (#28834) - #28853

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-i6gtvk
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-i6gtvk

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #28834

Requested by Leehom · project thread

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no file under skills/** or content/docs quotes either override refusal sentence or teaches an extension's extend spelling against a legacy ns__name key; skills/objectstack-data/SKILL.md teaches objectExtensions by a plain object name, which stays admitted.

维护者速读

  • 改了什么:① 给 object_extension 带 overrides 时的拒绝句改成「扩展不接受覆盖,名字就是它扩展的对象,请直接改这个扩展」,不再叫人「换个新名字另建」;给 object 带 overrides 的拒绝句在 manifest 的 objectExtensions 之外,也点出 /api/v1/meta/object_extension 这个保存口。② 保存/发布/回滚口拒绝 extend 写成短名、但注册表实际挂在旧式 ns__name 键上的扩展(以前会再开一个同短名的幽灵对象),拒绝句写出两种拼法。
  • 为什么改:PR feat(metadata-protocol,objectql): an object extension is an authorable item of a writable package, saved, published and exported through the existing doors (#28574) #28811 合约评审的 should-fix 1、4(卡 metadata-protocol: an object_extension item's refusal sentences, row comment and folded target spelling misfit the item door #28834)。第 3 项(spec 注册行注释)在 packages/spec/**,按裁决留给 spec 席位,本 PR 不碰。
  • 风险与回滚:拒绝码和状态不变(NOT_OVERRIDABLE/403、VALIDATION_ERROR/400);其他类型的拒绝句逐字不变(permission 对照钉住)。收窄只命中带 __ 的旧键,实测本仓库与 hotcrm 的 manifest 和对象都没有。回滚 = revert 本 PR。
  • 先看:packages/metadata-protocol/src/protocol-packaged-base-refusal.ts 的 judgeOverrideWrite 两处句子;packages/metadata-protocol/src/protocol-object-extension-item.ts 第 4 步新加的拼法判断。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/domains/meta-object-extension-item-door-fit.test.ts,预期 5 个测试全绿。
  • 需要你做的:无,席位安排合约评审并落地。

What changes

  • declared_type_not_overridable for object_extension (judgeOverrideWrite, protocol-packaged-base-refusal.ts). A new branch ahead of the D6 sentence, for this type only. The new sentence: 'object_extension/NAME' carries `overrides`, but an object extension takes no override (ADR-0146 D10): it adds to the object it extends and is stored under that object's name. Remove `overrides` and edit the object_extension 'NAME' itself. The rule name, code and status stay.
  • declared_object names both places an extension is written: ... add the change as an object extension in the base's paired .custom package: an `object_extension` item saved through `/api/v1/meta/object_extension`, or an `objectExtensions` entry of its manifest. The rule, code and status stay. This sentence is also what os validate and the install door print, so it names the manifest entry as well as the item door.
  • The target spelling (objectExtensionItemRefusal step 4, protocol-object-extension-item.ts). After getObject(extend) resolves, the gate refuses with VALIDATION_ERROR / 400 on extend when the resolved object's name (the key it is registered under, since computeFQN is the identity) differs from extend. The sentence names both spellings and says to save it under the registered key. The gate is the same one that save, per-item publish and history restore already call. ⛔ The manifest loop in objectql/src/engine.ts and computeFQN are untouched.
  • .changeset/28834-object-extension-door-fit.md: minor on @objectstack/metadata-protocol, both declaration lines, the ADR-0087 not-required (no-migration-prescription) marker and the BREAKING banner.
  • No new rule, no new OverrideWriteRule member, no new error code, no new gate, nothing under packages/spec/**.

Measurement before the narrowing (Wanted 3)

  • Shipped manifests. The non-test objectExtensions in this repository are plugin-auth (extend: 'sys_sso_provider') and the showcase (extend: 'showcase_account'). Each resolves to the same key, so neither is refused. hotcrm has no objectExtensions and no extend:.
  • Legacy keys. No non-test object in this repository or in hotcrm is named ns__name. The only extend with __ in the tree is a spec/lint test fixture (cbpx__cbpx_line), which names the key itself and so is admitted.
  • Stored rows. I have no deployed store to read. The item door these rows come through landed in metadata-protocol + objectql: an object extension is an authorable item of a writable package — saved and published through the existing doors, registered as 'extend', exported as objectExtensions #28574 (0c1829a28a), the change just before this one. The boot and read hydrations register stored rows through registrableObjectExtension and never call this gate, so a stored row with that spelling is not refused at rest. Only a re-save of it is. No live row was found that this would refuse, so the order's stop condition did not trigger.

Tests

packages/runtime/src/domains/meta-object-extension-item-door-fit.test.ts (new, 5 tests). It uses the rig of meta-object-extension-item.test.ts: a real better-sqlite3 SqlDriver, a real ObjectQL engine and the real protocol. The base com.acme.crm is registered through registerApp, owns crm_account and the legacy-keyed crm__case, and its pair is installed through installPackage. Every refusal asserts its envelope, its whole sentence and that no row was left.

Reading Result
Head 461552ac Tests 5 passed (5); with the #28574 file, 24/24
Base 0c1829a2 (both production files restored, the test kept, metadata-protocol rebuilt without the turbo cache) Tests 3 failed, 2 passed (5). Red: the extension sentence (base said a object_extension takes no override (ADR-0146 D6) ... create your own object_extension under a new name), the object sentence (no item door), the folded spelling (admitted, null). Green: the permission control and the registered-key control.

The tests:

  • the object_extension sentence, exactly, and not "new name" / "new similar";
  • the object sentence, exactly;
  • control: permission's D6 sentence, byte for byte;
  • the folded short name case refused in draft and in publish mode, with the issue on extend, no row left, and no contributor under case;
  • control: crm__case admitted and published, the pair as its extend contributor, region__c on the object.
Ablations (node scripts/ablation-replace.mjs from committed HEAD, metadata-protocol rebuilt without the cache, marker counted in dist)
  • The extension sentence branch. if (type === OBJECT_EXTENSION_TYPE) { guarded off (marker ABL_A, 1 in dist). Tests 1 failed, 4 passed: exactly the extension sentence. Restored: blob equals HEAD, git diff HEAD empty.
  • The item door in the object sentence. ${OBJECT_EXTENSION_ITEM_DOOR}, or replaced (marker ABL_B). Tests 1 failed, 4 passed: exactly the object sentence. Restored clean. The first attempt dropped the constant entirely, so the DTS build refused an unused local (exit 9, nothing measured). It was re-run with the constant kept as .slice(0, 0).
  • The spelling check. Its condition guarded off (marker ABL_C). Tests 1 failed, 4 passed: exactly the folded-spelling test. Restored clean.
  • After the last leg, metadata-protocol was rebuilt at HEAD: 0 markers in dist, and the card files are 24/24.
Suites, typecheck, lint and gates at head
Run Result
@objectstack/metadata-protocol 297 files passed, 4 skipped · 22090 tests passed, 26 skipped
@objectstack/objectql 522 files · 10009 tests
@objectstack/runtime 514 files · 9261 tests passed, 19 skipped
pnpm --filter @objectstack/metadata-protocol typecheck exit 0
pnpm --filter @objectstack/runtime typecheck (incl. check:test-typecheck) exit 0. The first run was exit 1 on organization-ownership-boot-guard.turso-remote.kernel.test.ts TS2307 because @objectstack/driver-turso had no dist/ in the fresh worktree; after building it, exit 0 with the ledger unchanged.
eslint --no-inline-config over the three changed TS files exit 0
check:type-check-debt exit 0
dispatch-gates.mjs --commands (61 families) all 61 exit 0 after a full pnpm build; --ran: 61 run, 0 NOT-MEASURED. The changeset and citation families were re-run after the last amend: exit 0. No gate added.
Full dogfood (narrowing arm), head 461552ac VERDICT command-exit 1 · Test Files 4 failed, 376 passed, 3 skipped (383) · Tests 4 failed, 3115 passed, 28 skipped (3147). The four reds are the 18.0-upgrade files audit-key-material, audit-mail-bodies, identity-mail-read-gate and upgrade-18x-harness (no such table: sys_activity__r20261019, rewrite counts halved). The same four files on base 0c1829a2, with metadata-protocol rebuilt at base: the same 4 red, 45 passed. So they are on main, tracked by #28849, and not this diff.

Acceptance notes

  • Wanted 2 is not here. The object_extension registry row's comment is in packages/spec/src/kernel/metadata-plugin.zod.ts, which the ruling sends to the spec seat. It is untouched and left for that seat. The PR feat(metadata-protocol,objectql): an object extension is an authorable item of a writable package, saved, published and exported through the existing doors (#28574) #28811 body's probe line objects: [] (the export omits the key) is on a merged PR, so it is noted here rather than edited.
  • Why name is the resolved key. registerObject files an object under computeFQN(namespace, schema.name), which is the identity, so the merged object's name is its key. A target named by its own key, legacy or not, is unchanged.
  • Branch order. The extension branch comes after the D10 object branch and before the D6 disposition check. object_extension is classified excluded, so before this change it reached the D6 sentence.

Verification

pnpm --filter @objectstack/runtime exec vitest run src/domains/meta-object-extension-item-door-fit.test.ts gives 5/5. The same command on base 0c1829a2, with metadata-protocol rebuilt, gives 3 red, as in the table.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5kkSYvKhs52WGfTj3c

…tem door

An object_extension body carrying `overrides` is refused with a sentence
that fits it: an extension takes no override and is stored under its
target's name, so the remedy is to edit the extension itself, not to
create one under a new name. An object carrying `overrides` is told both
places an extension is written: the object_extension item door and the
manifest's objectExtensions entry. Every other type's sentence is
unchanged.

The save, publish and restore gate now refuses an object_extension whose
`extend` is a short name getObject folds to a different, legacy
`<ns>__<name>` key. The registry files an extension under the name it is
written with, so that write opened a second, phantom object. The refusal
names both spellings.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 10 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 a31431824958c8e789427c4db593bcf867e8e051 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

Contract-review-claim: session_01R1q2xdtYgU6FKrZ7LgjPCC · head 461552acedd0d0070debccf845de1350d307af6b · 2026-11-02T01:34Z

Taken by the dispatching seat. Face narrowing (Clause-②: no (narrowing), Metadata-protocol: narrowed), at CONTRACT_REVIEW_TIER with ④. The review runs as an isolated sub-agent.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 461552acedd0d0070debccf845de1350d307af6b
Local-runs: none
Scope: packages/metadata-protocol/src/protocol-object-extension-item.ts, packages/metadata-protocol/src/protocol-packaged-base-refusal.ts, packages/runtime/src/domains/meta-object-extension-item-door-fit.test.ts + packages/objectql/src/registry.ts (computeFQN, parseFQN, openObjectEntry, resolveObjectKey / getObject, registerObject, registerObjectExtension, assertSingleOwnerPerObject), packages/objectql/src/engine.ts (the manifest objectExtensions loop, l. 9453), packages/metadata-protocol/src/protocol.ts (saveMetaItem gate order l. 21460 / 22148, publishMetaItem l. 24288, the objectExtensionItemRefusal host l. 7833, the registrableObjectExtension syncs), packages/metadata-protocol/src/protocol-authoring-gate.ts (restoredBodyWriter), packages/spec/src/kernel/customization-package.ts (OVERRIDE_TYPE_CLASSIFICATION), packages/spec/src/data/object.zod.ts (ObjectExtensionSchema.extend), docs/adr/0146 D6 / D10, packages/spec/src/kernel/metadata-plugin.zod.ts (the Wanted 2 row, read only). The changeset .changeset/28834-object-extension-door-fit.md is the prose face and is read for ②, not reviewed.

Rendered by the seat's subagent, reading the card (#28834, body and both comments), the PR (#28853 body, 4-file list, net diff origin/main...461552ac, merge-base 0c1829a2) and the 48 check-runs on the head at 2026-11-02T01:35Z. The dispatching seat's own Contract-review-claim: comment was not an input. The dev's os-dev-report is read as the dev's account, never as the tree.

Check-runs on the head (the gate verdicts)

48 check-runs, all completed at 2026-11-02T01:35Z; none in progress. 36 success, 12 skipped (UI build / type-check / lint / test shards, Build Docs, Dogfood Verify CLI, Temporal Conformance, the Dogfood Regression Gate shards, the Packed-tarball smoke, the 18.x upgrade harness opt-in, and the schedule gate: all path-filtered or opt-in), 0 failure. The gate families that decide: Check Changeset success, Governed Surface Queue Guard success, PR-body guards (claim · duplicate · Part-of · single-writer path) success, Lint & Repo Gates success (incl. Lint · PM dispatch-gates, Lint · docs & code guards), Type Check · workspace / source gates / consumer gates / debt ledger success, Test Core 1/8 … 8/8 success, Dogfood Regression Gate success, Flag docs affected by code changes success. The dev's four local dogfood reds (the 18.0 upgrade-harness files) are on main, carried by #28849; the harness job is opt-in and skipped on this head, and no CI check-run is red.

① Derived judgments

  1. Accept-set narrowing at the object_extension item doors — right. objectExtensionItemRefusal step 4 now refuses, VALIDATION_ERROR / 400 with issues[0].path === 'extend', a body whose extend resolves through registry.getObject to an object whose name differs from extend. Read in the cited code: getObject → resolveObjectKey answers the objectKeysByShortName index first (registry.ts l. 5942), which openObjectEntry fills with parseFQN(fqn).shortName (l. 3411), so a short name folds to a key only when that key carries __ — a legacy ns__name key, exactly as the hunk claims. registerObjectExtension (l. 6095) hands { ...body, name: extend } to registerObject, which files it under computeFQN(namespace, schema.name) — the identity (l. 472) — through openObjectEntry(extend): the written spelling, never the folded one. So before this change the admitted write opened an owner-less case entry beside crm__case, and the fields never reached the target. Nothing refused that entry: assertSingleOwnerPerObject (l. 6189) has no caller under packages/*/src (CHANGELOG mentions only). The refusal lands on the three doors that already ask this one function — saveMetaItem (protocol.ts l. 22148, draft and publish alike), publishMetaItem on the draft promoted (l. 24288), and restoredBodyWriter (protocol-authoring-gate.ts l. 117). publishPackageDrafts does not ask it, and did not before: a draft of the folded spelling cannot exist, since the save door refuses it. The typeof object.name === 'string' guard keeps the gate's fail-open direction for a registry shape without a name.
  2. declared_type_not_overridable sentence for object_extension — right. A branch keyed on type === OBJECT_EXTENSION_TYPE, placed after the extend_only (object) branch and before the generic row?.disposition !== 'overridable' D6 sentence. The spec classifies object_extension as excluded / adr_0146_d10_extended (customization-package.ts l. 341), so before this change it fell to the D6 sentence ("create your own object_extension under a new name"), which names a write the type cannot take: an extension's row name is its target (step 3 of the gate). Rule member, NOT_OVERRIDABLE / 403 and the OverrideWriteRule union are unchanged; only the message. Every other excluded type still reaches the D6 sentence (the permission control pins it byte for byte).
  3. declared_object sentence — right. Message only; the rule and envelope stay. The sentence is also what os validate and the install door print (the docblock on judgeOverrideWrite), and it now names both the manifest entry and the item door /api/v1/meta/object_extension, which metadata-protocol + objectql: an object extension is an authorable item of a writable package — saved and published through the existing doors, registered as 'extend', exported as objectExtensions #28574 opened. No code or status changes anywhere.
  4. Public surface — none added. OBJECT_EXTENSION_ITEM_DOOR is a module-private const. The new import of OBJECT_EXTENSION_TYPE into protocol-packaged-base-refusal.ts is acyclic (protocol-object-extension-item.ts imports nothing from it). No export, type, schema key, error code or gate is added; packages/spec/**, computeFQN and the engine.ts manifest loop are untouched, as the claim ruled.
  5. Not narrowed, and read as such — right by the claim, carried as a finding in ④. The manifest objectExtensions loop (engine.ts l. 9455–9474) still registers name: ext.extend verbatim, and the boot / read hydrations register a stored row through registrableObjectExtension without this gate, so the same misfiling stays reachable through a code package's manifest and for a row already at rest. The PR body says so, and the claim's ⛔ excluded both.
  6. The test file — right. Real SqlDriver + ObjectQL + protocol rig; the base ships crm_account and the legacy-keyed crm__case; the pair installs through installPackage. The override gate runs at saveMetaItem l. 21460 before the schema gate, so overrides on an object_extension body meets judgeOverrideWrite and not the strict parse's 422 — the sentences asserted are the ones the code emits (compared string by string against the hunks). The folded case is asserted in draft and publish-mode save with issues path, no row, no contributor under case, and the base's contributors unchanged; the registered-key control publishes and shows the pair as extend contributor with region__c folded in. The publish-of-a-stale-draft and restore arms of the same gate are not exercised here; they call the one function read above, so nothing is left unjudged.

② Semver level

.changeset/28834-object-extension-door-fit.md: "@objectstack/metadata-protocol": minor, the BREAKING banner, Clause-②: no (narrowing), Metadata-protocol: narrowed, and the ADR-0087 not-required (no-migration-prescription) marker. This matches what the diff publishes: the only published package touched is @objectstack/metadata-protocol (the runtime file is a test, not a surface), one accept-set narrowing and two message changes, no schema key, no export. The repo's other (narrowing) changesets on main carry patch or minor with the BREAKING banner, never major, and the Check Changeset gate is green on this head, so minor is the convention's level. The Clause-②: line reads the same on the PR body and the changeset (no (narrowing)), and both carry Metadata-protocol: narrowed. The changeset's Before / Now / Who-sees-it paragraphs describe the diff I read, not more. The ADR-0087 reason holds: no key of any schema is added, removed, renamed or reshaped, and a row at rest is not rewritten, so there is nothing for a migration to carry.

Clause-②: no (narrowing) — read as declared, verified against the diff above.

③ Boundary flags

  • open_questions A vs B (keep Fixes #28834 or narrow to Refs) — answered: A. Wanted 1 and 3 land here; the seat carried Wanted 2 (the object_extension row comment in packages/spec/src/kernel/metadata-plugin.zod.ts, read at l. 807–828) to the spec lane as spec: the object_extension registry row's comment says the artifact loader stores extensions under this type; it does not #28855, so closing the card loses nothing. Not counted as a gap of this PR.
  • Dev flag: no deployed store to read; a stored row of the folded spelling is not refused at rest — answered, accepted. The hydrations never ask this gate (read in registrableObjectExtension's callers), so such a row keeps opening its phantom entry silently on boot; its re-save, publish or restore is now refused with a sentence naming the registered key, and the author's path is delete the row and save it under that key. The measurement (the two shipped manifests extend their own keys; no non-test object named ns__name in this repo or hotcrm) is the dev's, and the tree shows the same two extend targets. Carried into ④ as a should-fix on the manifest and at-rest paths, not this PR's.
  • Dev flag: 4 dogfood reds in the 18.0 upgrade-harness files — on main, carried by 4-hourly full run: red on main (CI) #28849; the CI harness job is opt-in and skipped on this head; the Dogfood Regression Gate and every Test Core shard are green. Not this diff.
  • Dev flag: PR feat(metadata-protocol,objectql): an object extension is an authorable item of a writable package, saved, published and exported through the existing doors (#28574) #28811's body probe line objects: [] — prose on a merged PR; nothing in the tree to fix. Noted, no action.
  • The declared_object sentence names /api/v1/meta/object_extension even when printed by os validate or the install door (manifest contexts with no running door) — the sentence names both places, so a manifest author still reads the manifest remedy first. Not escalated.

④ Design judgment

Judged against the published spec on main: ObjectExtensionSchema.extend is described as "Target object name (FQN) to extend" (object.zod.ts l. 4944), ADR-0146 D10 says an extension is "authored into .custom" and merged by mergeObjectDefinitions into the object it names, and the registry's own contract (registerObjectExtension's docblock) is that extend "becomes the contribution's name (the object it registers under)".

  • Does the narrowing take a declared capability away at the consumer? No. The spelling it refuses — a short name whose only match is a legacy ns__name key — was never a delivered capability at this door: the registry never folded it, so the write registered nothing on the target and opened an entry with no owner, which no invariant in the live tree checks. The spec's extend is the registered name, and the refusal makes the registry's own rule real at the write door, with a sentence that names the key to use. This is the right side of the line: a refusal of a write that never did what it said, not a narrowing of a delivered path.
  • Was implementing rather than refusing the better fit? Folding at registerObjectExtension (resolve through resolveObjectKey) would deliver the write silently, but it would re-key a row stored under one name to another registry key, breaking step 3's "the row name is the target" and the one-extension-per-package-per-target identity the item door was built on (metadata-protocol + objectql: an object extension is an authorable item of a writable package — saved and published through the existing doors, registered as 'extend', exported as objectExtensions #28574), and leaving a GET that answers a different name than the registry serves. Refusal, as the card's Wanted 3 ordered, is the coherent choice. Not a finding.
  • Trying to break it. (a) Ambiguous short name (two legacy keys sharing a short name): resolveObjectKey warns and returns the first; the refusal names that key — still a refusal, still a real key, and an author who meant the other reads the one-key sentence and writes the other key explicitly. Not a finding. (b) A tenant-authored owner (an object row) keyed by body.name: name equals the key, admitted as before. (c) Step order: the spelling check sits before the closure check (5), so a folded spelling outside the closure gets the spelling sentence first — the more actionable of the two. (d) The override branch order: object (extend_only) is judged before object_extension, and the new branch is keyed on the type constant rather than the classification row, so a future reclassification of object_extension would still route here; acceptable, since the sentence is about the type's nature (no new name), not its row. (e) The sentence change does not reach the generic D6 sentence for any other type — confirmed by the control and by reading the branch.
  • should-fix (carried, not this PR): the same misfiling remains on the manifest path — engine.ts l. 9455 registers name: ext.extend verbatim for a code package's objectExtensions — and on the at-rest hydration of a stored row. A manifest with extend: 'case' against a legacy crm__case key passes os validate and the install door and opens the same phantom. The claim ⛔ excluded the manifest loop, and the card's Wanted 3 named the save gate only, so this is the objectql lane's own small card, with the same measurement (no shipped manifest or object reaches it today).

No must-fix.

Implemented-by: claude/project-thread-i6gtvk
Reviewed-by: session_01R1q2xdtYgU6FKrZ7LgjPCC

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

席位核对 · ACCEPT · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-11-02T01:43Z

Head 461552ac, report 6566753086 (on #28834), claim 6565564753. Clause-②: no (narrowing), Metadata-protocol: narrowed. The contract review at CONTRACT_REVIEW_TIER (narrowing face) PASSED on this exact head, record 6567063545, with no must-fix.

item reading
Shape Draft, base main, Fixes #28834. 4 files. The object_extension override refusal now says to edit the extension itself, and declared_object names /api/v1/meta/object_extension beside the manifest entry; other types' sentences are unchanged. The item gate refuses an extend that getObject folds to a different key (400 on extend, naming both spellings). No new rule, code, export, gate or packages/spec/**. A minor changeset with the BREAKING banner.
Out of this PR Wanted 2 (the registry row comment, in packages/spec/**) went to the spec lane as #28855. The review's one should-fix (the manifest loop and at-rest hydration still file extend verbatim) is outside this PR's claim, filed as #28858.
Evidence CI on head 461552ac (filter=latest): 13 skipped, 35 success, 0 failed. The report shows 3 red on main and 5/5 on head, 3 ablations each restored clean, the metadata-protocol, objectql and runtime suites, both typechecks in full, and 61/61 derived gates. The dev's 4 local dogfood reds are main's (p0 #28849).
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, with every check green or an expected skip.

Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

metadata-protocol: an object_extension item's refusal sentences, row comment and folded target spelling misfit the item door

1 participant