Skip to content

fix(metadata-protocol): duplicatePackage keeps a copied field's key and name, and a bare object's view item name, in step with what they name - #23559

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23538-duplicate-names-in-step
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23538-duplicate-names-in-step

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23538
Clause-②: no

What this does

duplicatePackage (packages/metadata-protocol/src/protocol.ts, ADR-0070 D4) promises to rewrite "every intra-package reference". Three copied names fell out of step with what they name. All edits are in the duplicatePackage region: the free-text rename, the row-name rename and deepRewrite. The source-row read is untouched.

  1. A field's key and its name agree. The free-text regex had a right identifier boundary and no left one, and an object's fields keys are never renamed outside a reference site. So the field keyed dupsrc_customer was copied with name: 'dupdst_customer', and x_dupsrc_customer with name: 'x_dupdst_customer'. Two rules now hold:
    • A record entry's name that echoes its key follows the key (deepRewrite). Both are kept, "through none", the reading consistent with the landed permission-field rule ("the field part is never read").
    • Free text renames an object's name only as a whole identifier. A left boundary is added: a negative lookbehind that refuses a match preceded by a letter, digit or underscore. x_dupsrc_customer stays in a description and in a view's columns, the way renameName swaps the prefix only at the start of a name.
  2. A view item's OBJECT.VIEWKEY name follows its object. For a view row with a qualified name, the object part takes renameMap's rename, whole, and the view key is left as stored: ticket.board copies as dupdst_ticket.board. That is exactly the rename its object property takes. An object the copy does not rename keeps its items' names. This also covers an outside object that carries the source prefix: dupsrc_ext.board used to become dupdst_ext.board while its object stayed dupsrc_ext.
  3. The noted view-item id inside a value reproduced, so it is carried here (triage scope). A value that is a renamed view item's whole name follows it: a form action's target: 'ticket.entry' becomes dupdst_ticket.entry. This applies to whole values only, and only to names of view items this copy renames.

Mechanism assumptions (Zone 2), measured on origin/main 8226755ee9:

  • Item 1: partly confirmed. The missing left boundary explains x_dupsrc_customer only. A value EQUAL to the object's name (dupsrc_customer) needs no left boundary to match, so the left boundary alone does not satisfy the Done line. The field's name has to follow its key's decision. Both changes landed, each with its own pin and ablation.
  • Item 2: confirmed. renameName renames only names with the source prefix. A bare object's item kept ticket.board, and getMetaItem({ type: 'view', name: 'ticket.board' }) served the source (packageId: com.qa.dupsrc, object: ticket).

Evidence (real kernel: ObjectKernel + ObjectQLPlugin, memory driver)

New pin file packages/objectql/src/protocol-duplicate-package-names-in-step.test.ts, 5 cases, each with its negative control:

case on main 8226755ee9 on this head
field key and name agree; x_dupsrc_customer keeps both (neg. control); the data plane inserts by the key; the copied set's FLS rules land red: failed[] holds dupsrc_ops, security-fls-unknown-field on dupdst_customer.dupsrc_customer and dupdst_customer.x_dupsrc_customer green
free text renames only a whole identifier (description, view columns) red: see x_dupdst_customer. green
bare object's view item is dupdst_ticket.board; the by-name read serves the copy, and ticket.board serves the source red: copy stored as ticket.board green
view items of objects outside the package keep their names (dupext_account.board, dupsrc_ext.board) red: dupsrc_ext.board copied as dupdst_ext.board green
a form action's view-item target follows (prefixed control; outside-object negative control) red: target: 'ticket.entry' green

Main run: Tests 5 failed (5). Head run: Tests 29 passed (29) over the five duplicate suites (names-in-step, unprefixed-object, permission-field-keys, objects-key-collision, protocol-package-lifecycle).

Ablations. One per mutation, each through scripts/ablation-replace.mjs (anchor hit 1 to 0, blob changed). Then pnpm --filter @objectstack/metadata-protocol build and ablation-dist-preflight (marker present in 2 built files). Then the pin file. Then the restore, proven by blob == HEAD 3d7ac7e2e7b3 and an empty git diff HEAD, then a rebuild and --absent preflight (marker absent from all 24 built files, tree clean).

ablation predicted observed
A1: drop the left boundary case 2 red 4 red, 1 green: more diagnostics than predicted. The copied list view's column x_dupdst_customer names no field of the copy, so the save door refused that view (list-view-field-unknown), which also reds cases 1, 3 and 4
A2: drop "name follows key" case 1 red 1 red (case 1), 4 green
A3: view row back to the prefix swap cases 3, 4 red 2 red (cases 3, 4), 3 green
A4: drop the whole-value view-item rename case 5 red 1 red (case 5), 4 green

Tests and gates (final head dbd69f7592, after merging origin/main 086fa4640f)

  • pnpm --filter @objectstack/metadata-protocol exec vitest run: Test Files 236 passed | 3 skipped (239), Tests 28409 passed | 19 skipped.
  • @objectstack/objectql --project local, as two shards: 210 + 210 files, Tests 4052 passed + Tests 4067 passed.
  • pnpm --filter @objectstack/metadata-protocol typecheck exit 0. pnpm --filter @objectstack/objectql typecheck exit 0 (check:test-typecheck: OK).
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 67 commands from a fresh tree. Every exit was captured before any pipe. --ran printed: 67 derived famil(ies) accounted for — 66 run, 1 NOT-MEASURED.
    • The one is pnpm check:dual-build-cjs-loads, which exited 3 with PREREQUISITE NOT MET (it needs every package's dist/; a full pnpm build is outside this card's local scope). NOT MEASURED locally; CI's Lint & Repo Gates runs it.
    • The other 66 exited 0.

Acceptance notes

Changeset: .changeset/23538-duplicate-names-in-step.md, @objectstack/metadata-protocol patch.

Session session_0121ooBYRmS8ziFXm9ai5YHm, os-dev for domain:spec seat 4.


Generated by Claude Code

…nd name, and a bare object's view item name, in step with what they name

A field's `name` that echoes its key follows the key, the free-text
rewrite matches an object's name only as a whole identifier (left
boundary added), a qualified view item's object part takes the copied
object's name, and a value equal to a renamed view item's whole name
follows it.

Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm
Co-authored-by: Claude <noreply@anthropic.com>
…nameName's start-of-name swap

Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm
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 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 4 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 7ffc57846a97afde24fbfc59b6b05d7562ef4df0 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 7ffc57846a97afde24fbfc59b6b05d7562ef4df0

⚠️ 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 7ffc57846a97afde24fbfc59b6b05d7562ef4df0 → 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:spec seat 4 (#18917) · sunhaolin · session session_0121ooBYRmS8ziFXm9ai5YHm · claim 6180510765 on #23538 · reviewed head dbd69f7592 · 2026-10-15T00:20Z

Verdict: ACCEPT. Not governed (check-governed-merges --pr 23559). 4 files, +435 / −10. Clause-②: no: these are producer fixes, and no door changes what it accepts. No contract review is owed; this record is the review.

  1. Shape. Draft PR. The first line is Fixes #23538, and Clause-②: no sits at line start. The changeset is @objectstack/metadata-protocol patch.
  2. Scope against the claim.
  3. The change, read in the diff.
    • Both identifier boundaries. The free-text rewrite now matches an object's name only as a whole identifier, with a left lookbehind added to the right one. So x_dupsrc_customer stays.
    • A record entry's name follows its key. When a name echoes its key, it takes the key's rename decision, so a copied field's key and name agree.
    • A view item's name follows its object. A qualified <object>.<viewKey> row takes renameMap's rename of its object part, whole, and keeps its view key, so ticket.board becomes dupdst_ticket.board. An item of an object this copy does not rename keeps its name. That includes a prefixed outside object, which the old prefix swap moved.
    • Whole values follow too. A value equal to a renamed view item's whole name follows it, for example a form action's target.
  4. Evidence.
    • The pin file: 5 real-kernel cases, all red on main 8226755ee9, each at its defect, and all green at the head.
    • The five duplicate suites: 29 passed.
    • Full suites: @objectstack/metadata-protocol 28409 passed / 19 skipped, and @objectstack/objectql (in two shards) 4052 + 4067 passed. Typecheck exit 0.
    • Ablations, one per mutation, each red, each restore proven:
      • A1, the left boundary, went red on 4 cases where 1 was predicted, in the louder direction: a copied list-view column is refused at the save door;
      • A2, the name follows its key: 1 red;
      • A3, the view row's prefix: 2 red;
      • A4, the whole-value rename: 1 red.
    • Local gates: 67 derived, 66 run, 1 NOT MEASURED (check:dual-build-cjs-loads, prerequisite not met; CI runs it).
  5. The dev's open question: A, as built. The residual, a token that names both a prefixed package object and a field, is measured and not fixed here. It has zero measured producers across the three examples (51 object names, no field key equal to one), and B and C each pick a semantics for an ambiguous token with no measured case. It is filed as this family's close-out card. The view-container row note (read, not measured; not a contract breach) is an Acceptance note, carrier: none.
  6. Landing clauses.
    • (a) Every check is green or skipped by design: read at landing, after the skip roster answers OK. 15 runs were in progress at this record.
    • (b) mergeable_state is clean at enqueue: read at landing.

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