Skip to content

fix(metadata-protocol): duplicatePackage renames a name that is also a field key only at object-reference sites - #24475

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23561-duplicate-object-name-token
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23561-duplicate-object-name-token

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23561
Clause-②: no

What this does

duplicatePackage (packages/metadata-protocol/src/protocol.ts, ADR-0070 D4) renamed a token that is BOTH a prefixed package object's name AND a field key as if it were the object, in free text. A field keyed dupsrc_customer on the object dupsrc_customer kept its key on the copy, but a copied list view's columns and a copied form's sections[].fields were rewritten to dupdst_customer, a field the copy does not have. Since PR #23559 the list view was refused at the copy (list-view-field-unknown). The form copied silently, with a dangling field.

Triage ruling (comment 6181968665), direction B: "Rename such a name only at object-reference sites, never in free text. That extends the landed bare-name policy". C (also renaming the field) is ruled out.

The mechanism: before the free-text regex is built, duplicatePackage collects the fields record keys of every object row in the source package. A prefixed renamed object name that is also one of those keys is left out of the free-text regex. Such a name is then rewritten only by the existing object-reference path (REFERENCE_SITES target object: object, reference, objectName, a permission set's objects keys, and the rest), which is exactly how a bare object name is already handled. No second mechanism is added, and reference-sites.ts is not edited.

  • Scope of "a field key": package-wide. The set holds a field key on ANY object of the package, not only on the same-named object. For example, a lookup field acme_customer on acme_order raises the same ambiguity in that order's list view. Package-wide is the superset of the two readings.
  • Consequence of the ruling, pinned: free text (an object's description) naming such an object is now left as stored. A prefixed object that no field key spells is still renamed in free text, as a whole identifier, as before.
  • No accept/refuse change beyond the copy no longer refusing that list view.

Pins (packages/objectql/src/protocol-duplicate-package-names-in-step.test.ts, real kernel: ObjectKernel + ObjectQLPlugin, memory driver)

  • New case: a name that is both a prefixed object and a field key is renamed only at object-reference properties. It asserts:
    • res.failed is empty (the list view is no longer refused) and both views are copied;
    • the list-view columns stay ['full_name', 'dupsrc_customer', 'x_dupsrc_customer'];
    • the form's sections[0].fields stay ['full_name', 'dupsrc_customer'];
    • the object references DO follow the copy: the view object and config.data.object, two lookup references, the action objectName, and the permission set's objects key;
    • the description is left as stored;
    • engine.insert through the copied field key lands.
  • Flipped pin, load-bearing: the landed case-2 assertion that the description reads Linked from dupdst_customer now reads the description unchanged, by the ruled semantics. The whole-identifier (left-boundary) rule it also guarded is re-pinned on a new prefixed object with no matching field key, dupsrc_note. That case expects Attached to dupdst_note; see x_dupsrc_note., and the field x_dupsrc_note keeps both its key and its name. So the guarded surface does not shrink.
  • Negative control: dupsrc_note, an ordinary prefixed object, is still renamed in free text.

Ablation (through scripts/ablation-replace.mjs). The anchor renameMap.has(name) && !fieldKeys.has(name) went from 1 hit to 0 and was replaced by a marker literal (blob 49c970959c61 to 1ea7b74ba948). Then: rebuild @objectstack/metadata-protocol, and ablation-dist-preflight found the marker in 2 built files. The pin file then read Tests 5 failed | 1 passed (6). That is MORE diagnostics than the 2 red cases predicted: the copied list view's dupdst_customer column is refused at the copy, so failed[] is non-empty and cases 1, 3 and 4 go red as well. Restore: blob == HEAD, git diff HEAD empty, rebuild, --absent preflight (marker absent from all 24 built files), then the pin file read Tests 6 passed (6).

Tests and gates (head e7a7b375ed, after merging origin/main 26501ae24f)

  • Build: the @objectstack/objectql closure was rebuilt after the merge (VERDICT command-exit 0).
  • Five duplicate suites (names-in-step, unprefixed-object, permission-field-keys, objects-key-collision, package-lifecycle): Test Files 5 passed (5), Tests 30 passed (30).
  • pnpm --filter @objectstack/metadata-protocol exec vitest run: Test Files 246 passed | 3 skipped (249), Tests 20655 passed | 19 skipped.
  • Typecheck: pnpm --filter @objectstack/metadata-protocol typecheck and pnpm --filter @objectstack/objectql typecheck both exited 0 (check:test-typecheck: OK).
  • ESLint, a proven narrowing to the 2 touched source files:
    • --no-inline-config --format json gives 2 files, 0 errors, 0 warnings.
    • The config never enables type-aware linting (eslint.config.mjs, no parserOptions.project), so this diff cannot move any untouched file's verdict.
  • @objectstack/spec test:repo: Test Files 61 passed (61), Tests 1033 passed (1033). This was measured at efed56ba5b, before the merge. This diff touches no packages/spec file.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 69 commands at e7a7b375ed. Every exit was captured before any pipe. --ran printed: 69 derived famil(ies) accounted for — 68 run, 1 NOT-MEASURED.
    • The 68 that ran exited 0.
    • NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET. It needs every package's dist/, and a full build is outside this card's local scope. CI's Lint & Repo Gates runs it.

Acceptance notes

Changeset: .changeset/23561-duplicate-object-name-token.md, @objectstack/metadata-protocol patch.

Session session_01D6F9axfoPzR7t8G1J7Ytzz, os-dev for domain:spec.


Generated by Claude Code

…a field key only at object-reference sites

A prefixed package object's name that is also a field key of a package
object is no longer rewritten by the free-text rename: it may name the
field (a list view's columns, a form's sections[].fields), which keeps
its key. It is rewritten only at object-reference properties, as a bare
object name already is.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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 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 31ce45c071bbcb6e92a730ce19ebf30dcfa361cb → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 31ce45c071bbcb6e92a730ce19ebf30dcfa361cb

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Review: ACCEPT · PR #24475 @ e7a7b375ed6e61d61346c7e4eae7cf7c93542b3b · domain:spec seat 3 · 2026-10-19T06:15Z

Session session_01D6F9axfoPzR7t8G1J7Ytzz · baozhoutao. Reviewer of record; checked against GitHub, not the report.

  • Shape: draft, Fixes #23561, Clause-②: no; +89/−7, 3 files; not governed.
  • The ask (triage 6181968665, direction B): a prefixed object name that is also a field key of a package object is left out of the free-text rewrite, so it is renamed only at object-reference properties — the landed bare-name path extended, not a second mechanism. reference-sites.ts is untouched.
  • Pins bear load: the copied list view's columns and form's sections[].fields keep the field key; object, config.data.object, fields.*.reference, an action's objectName and a permission set's objects keys ARE renamed; the copy is not refused (failed empty); free-text description stays as stored; the whole-identifier rule is re-pinned on an unambiguous object.
  • Changeset: 23561-duplicate-object-name-token.md, @objectstack/metadata-protocol patch.
  • Gates: 69 derived, 68 exit 0; check:dual-build-cjs-loads not measured locally (needs every dist); CI measures it. Ablation turned 5 pin cases red and restore turned them green.
  • CI on the head: read at this act; the seat lands on green through the merge queue.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 06:16
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 19, 2026 06:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit e67ac4b Oct 19, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23561-duplicate-object-name-token branch October 19, 2026 07:12
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/s tests tooling

Projects

None yet

2 participants