Skip to content

fix(objectql): the object fold names a field two extensions declare, and which definition it serves - #28876

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-3l6p3b
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-3l6p3b

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #28835

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:对象合并时,两个扩展在 fields 里声明了同名字段,或某个扩展在 fields 里重新声明了所有者自己的字段,注册表现在会在日志里记一行:对象、字段、按合并顺序列出的所有声明包,以及最终生效的是哪个包的定义。同一注册表只记一次。
  • 为什么改:合并按 priority 升序、同优先级按注册先后,后者直接覆盖前者,之前没有任何提示。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 开放 object_extension 条目后,Studio 也能走到这条路径。
  • 风险与回滚:只加一行 warn,生效的定义一个字节不变,不拒绝任何东西,保存、发布、os validate 都没动。仓库里出货的包和示例没有一处这样冲突。回滚即还原本 PR。
  • 先看:packages/objectql/src/registry.ts 的 reportRedeclaredFields,以及 foldExtendersOntoDefinition 循环里新增的 folded.push 一行。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/registry-extension-redeclared-field.test.ts,预期 10/10。
  • 需要你做的:无,席位落地。

What changes

  • One warning at the fold. foldExtendersOntoDefinition records each extension's fields as the merge took them (after the audit and queue governance), in fold order. After the loop, reportRedeclaredFields finds every field name declared by more than one contributor: two extensions, or an extension and the owner (a name in ownFieldNamesOf(owner), the set the field-patch judge already uses). For each, it writes one console.warn naming the object, the field, every declaring package in fold order (the owner first when it declares the field), and the package whose definition is served, which is the last one the fold applies.
  • Once per registry. Keyed object|field|packages in a per-registry set, beside fieldPatchMissesWarned. Repeated reads (getObject, foldObjectExtendersOnto, the code layer) do not repeat it; a second registry says it again.
  • Judged from the contributors, not the running body. A base that already carries the extensions' fields (a folded body handed back to foldObjectExtendersOnto, a stored overlay row) is not read as a redeclaration.
  • Not counted. fieldPatches (not a declaration; a patch that misses is already named). A .custom pair's __c field and a dependent's plain field never share a name, so they never meet here; the __c judge is untouched.
  • Unchanged. The merged definition, byte for byte, and every door: no change to save, publish, restore, os validate or the install door.
  • Docs. content/docs/data-modeling/object-extensions.mdx said "nothing warns when they do"; it now says what the log names, and that the one registered last wins at equal priority.

Why

The PR #28811 contract review (record 6565271420, should-fix 2) found that two extenders of one object declaring the same field name resolve last-writer-wins with no refusal and no report. The seat's route: report at the fold, keep today's winner, refuse nothing.

Tests

packages/objectql/src/registry-extension-redeclared-field.test.ts, 10 cases.

Warning cases (each asserts the served definition first, then the line):

  • two dependents both add region at priority 200: the one registered last is served, one line names both and the served one;
  • the same by priority: the 300 registered first is served and named;
  • an extension redeclaring the owner's industry: one line names the owner, the extension and the served one;
  • the owner and two extensions on one field: one line naming all three in fold order;
  • two .custom pairs both adding region__c;
  • a second registry names it again.

Controls (nothing named): one extension adding its own field; a pair's region__c beside a dependent's region; fieldPatches beside an added field; a folded body handed back to the fold. Every case reads getObject, foldObjectExtendersOnto and getArtifactItem three times with the cache dropped between.

Red first and ablation (head ce7c7fa)
  • Red first. registry.ts checked out from main 7e60dc23: Tests 6 failed | 4 passed (10). All six fail on the line count (expected [] to have a length of 1 but got +0); the served-definition assertions before them pass, so the winner is the same on main. Restore: git checkout HEAD -- packages/objectql/src/registry.ts, git diff HEAD empty, porcelain empty.
  • Ablation (scripts/ablation-replace.mjs, the this.reportRedeclaredFields(...) call replaced by void 0), run over this file and registry-extension-field-patches.test.ts: Tests 6 failed | 21 passed (27), the same six warning cases; the four controls and objectql/cloud-connection: an option patch on an owner field that an extension takes over through fields applies in the registry fold but is refused at the install door and by os validate #28809's 17 stay green. ok restored: blob == HEAD (c47d8f140f3c) and git diff HEAD is empty, marker count 0.
  • The test imports ./registry.js from src/, so no dist/ leg is involved.
Suites, typecheck, lint, gates (head ce7c7fa)

After merging main (with #28836) and a whole-repo pnpm build (VERDICT command-exit 0):

  • objectql full suite: Test Files 524 passed (524), Tests 10036 passed (10036).
  • objectql pnpm typecheck: exit 0, check:test-typecheck: OK (39 files / 233 errors / 64 signatures held, unchanged).
  • eslint --no-inline-config on the two touched source files: exit 0.
  • Tests that spy on console.warn beside an extend registration in other packages: rest rest-exposure-gate-one-object-read 37/37, runtime package-uninstall-store-refusal.integration 5/5, cloud-connection marketplace-install-local-field-patch-targets 8/8.
  • dispatch-gates.mjs --commands: 92 derived, 92 run, all exit 0. --ran: 92 derived, 92 run, 0 NOT-MEASURED.
  • check-nul-bytes.mjs: exit 0.

Acceptance notes

Verification

pnpm --filter @objectstack/objectql exec vitest run src/registry-extension-redeclared-field.test.ts

Expect Tests 10 passed (10).

Human Minimum

  • 1. No packages/spec change.
  • 2. New names follow their kind's casing.
  • 3. No formatter ran; eslint on touched files exits 0 (pnpm lint is CI's).
  • 4. One issue, one branch, one pull request.
  • 5. Changeset present; both declaration lines filled in here and in it.
  • 6. packages/spec not touched.
  • 7. No hand edits to references, changelogs or release notes.
  • 8. English throughout, except the 维护者速读 block the fleet's PR contract requires.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5nbGTknnd7qupVJNmN

…lds, with the definition served

The registry fold spreads each extension's fields, so a field name two
extensions declare, or an extension redeclaring an owner field, serves
the last contribution the fold applies. The fold now writes one warning
per object, field and set of packages, naming every declaring package
and the one served. The served definition is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nbGTknnd7qupVJNmN
…field branch

# Conflicts:
#	packages/objectql/src/registry.ts
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/concepts/metadata-lifecycle.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/protocol/kernel/plugin-spec.mdx (via SchemaRegistry (symbol, a top-level class))

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

  • content/docs/releases/v17/17-4.mdx (via SchemaRegistry (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 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 — 20 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 69c9f7c61a404bb7415f12f47878714aff411c99 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 69c9f7c61a404bb7415f12f47878714aff411c99

⚠️ 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 69c9f7c61a404bb7415f12f47878714aff411c99 → 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

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-02T02:22Z · head ce7c7fa3 · #28835

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
    • A draft against main with Fixes #28835 and a patch changeset for @objectstack/objectql, declaring Clause-②: no and Metadata-protocol: none.
    • Not governed, +301 / -4. Landing tier S.
    • The claim (6565899383) names this PR's branch, claude/project-thread-3l6p3b.
  2. Against the order (route: report at the fold):
  3. Seat's read: Clause-②: no holds by the diff, since it adds one private method, one private memo, a warning and a docs line. The judgment reads the contributors' own declarations, never a folded body, so a stored overlay row is not read as a redeclaration.
  4. Contract review: not owed (Clause-②: no, no spec path, no governed path).
  5. Gates: the dev reports 92 of 92 derived gates and objectql at 524 files / 10036 tests. CI on this head has 0 red so far, and auto-merge waits for the rest.
  6. Landing: pr_ready + automerge_enable through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 02:23
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 02:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 581d294 Nov 2, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-3l6p3b branch November 2, 2026 03:02
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.

objectql: two extensions of one object declaring the same field name merge last-writer-wins with no refusal or report

1 participant