Skip to content

ci(docs-drift): state the same-name-different-shape trap where the hand-grep remedy is prescribed - #19110

Merged
os-bill merged 1 commit into
mainfrom
claude/issue-19093-tools-name-collision-discriminator
Sep 18, 2026
Merged

os-bill merged 1 commit into
mainfrom
claude/issue-19093-tools-name-collision-discriminator

Conversation

@os-bill

@os-bill os-bill commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Fixes #19093

Clause-②: no — the diff is workflow prose plus its pin. No schema, no accept set, no public surface.

What this lands

The Docs Drift Check's fold already carries one unconditional boundary line, and it ends by
prescribing a remedy: "a rule your change carries has to be re-read by hand in the pages
that restate it". That remedy is a name-based search, and this card is about the fact that a
name does not identify a key. So the remedy's own failure mode now renders beside the
remedy
, on every run, with the censused names as its payload and the discriminating parse
as its fix — plus a byte-exact pin in scripts/docs-audit/check-drift-comment.mjs, the same
shape the neighbouring unconditional line takes.

The census — first deliverable, and it chose the site

Measured on this branch by walking the liveness ledger's governed types through the same
registry and the same shape helpers check-liveness.mts uses, and reading each key's
[REMOVED] marker. Names, never a count — on this card a count is structurally not a
reading, because the whole defect is that a count cannot tell two identically-named keys apart.

A key tombstoned on one governed type and authorable on another:

name tombstoned on authorable on
active flow agent, skill, datasource, app, email_template, validation, sharing_rule
aria dashboard, app action, page
joins query analytics_cube
objects app permission, translation, manifest
template flow page
tools agent skill
version app flow, manifest

So tools is one of seven, not a singleton — which settles the card's "what is NOT
established: how many other keys carry the same collision".

Radius of the instrument. Top-level keys of the governed types, resolved through
getMetadataTypeSchema() plus the SPEC_ONLY_SCHEMAS overrides, tombstone detected as a
[REMOVED] description prefix. Two things follow from that radius and both are stated in the
landed text:

  • Drilling the same walk to depth 2 widens the class well past these seven (timeoutMs is
    tombstoned at flow.nodes.waitEventConfig.timeoutMs while flow.nodes.timeoutMs is
    authorable — a collision between siblings on one type); depth 3 and beyond adds only
    query.expand.* self-recursion, so depth 2 is where the widening stops being new.
  • One known target outside the radius, measured: a key retired through the .strict()
    guidance-map route instead of retiredKey() leaves no tombstone in the walked shape at
    all
    , so this instrument cannot see it in either direction. tool.category is retired that
    way (TOOL_RETIRED_KEY_GUIDANCE in packages/spec/src/ai/tool.zod.ts) while
    AIToolDefinition.category in packages/spec/src/contracts/ai-service.ts stays live and is
    documented as carrying action.ai.category — the identical trap, invisible here. The same
    route hides a second instance of a name this census already flagged: tool.active.

The discriminating parse, re-derived first-hand

The card records that the dual-parse was measured by the reporting dev and not re-run by
the filing seat. It has now been re-run on this branch, against SkillSchema and AgentSchema
imported from source, with the object literal taken verbatim from
content/docs/ai/agents.mdx:155-162.

  • as a SkillSchema: PASS.

  • as an AgentSchema: FAIL, with an issue at ["tools"] whose message opens, verbatim:

    agent.tools was removed in @objectstack/spec 17 — use skills. An agent reaches exactly
    the tools its surface-compatible skills declare (ADR-0064), so move each reference into a
    skill: a platform tool by its registered name, or action_ACTIONNAME for one of your own
    AI-exposed Actions. This is NOT a rename — there is no key the value moves to: the migration
    DELETES the key and emits a notice naming each tool that was listed, and you re-declare each
    one in a skill by hand.

    (One edit to that quotation: the message spells its placeholder with angle brackets, which
    this repo's body-sanitizer discipline keeps out of a PR body — it is written action_ACTIONNAME
    above. Everything else is byte-for-byte what the parse emitted.)

One correction to the card, from that re-run. The card says the agent parse fails "with
exactly one issue". On the literal as written on the page it fails with two: ["tools"]
and ["role"] (invalid_type, required and absent — role is required on AgentSchema and
the page's skill block naturally has none). The "exactly one" reading holds only once every
AgentSchema-required key is supplied, which is presumably what the reporting dev did. Two
controls settle it:

  • Control A — literal + role, parsed as an agent: FAIL, exactly 1 issue, at ["tools"].
  • Control B — the same object with tools deleted, parsed as an agent: PASS, 0 issues.

So tools is the sole cause, and the direction is the one the card claims. The substance is
confirmed; only the issue count was off.

Control C, on the card's claimed hard discriminator: SkillSchema declares
triggerConditions and AgentSchema does not. Also worth recording because it is the
mechanism of the whole trap: AgentSchema does declare tools — the tombstone keeps the
key in the walked shape, which is exactly why a name-based search finds it there.

Page structure, re-read first-hand: the page's only tools: authoring occurrence is
:161, inside the defineSkill({ block opened at :155; the four defineAgent( blocks at
:256, :292, :325, :361 write zero tools.

Why this site, and why not the other

The card offers two candidate sites, marked as input and not a prescription. This PR takes the
Docs Drift Check blind-spot text and deliberately does not write the liveness ledger's
_note.

  1. The discriminator has to be on the path the trapped reader is already walking. The
    measured instance is a reviewer of PR fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059 who grepped content/docs and read the hit.
    That reviewer meets the Docs Drift Check comment on the PR, unconditionally, at the moment
    the hand-grep method is prescribed to them. They do not open a ledger file to decide whether
    a grep hit is real.
  2. The free half of the ledger site is the wrong half. A reader alarmed about agent.tools
    opens agent.json. agent.json is held by open PR fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059 and is fenced for this card, so
    the only writable ledger site is skill.json — the half that reader has no reason to open.
    A discriminator there is a warning posted on the door nobody walks through.
  3. The remedy and its failure mode belong in one place. The existing unconditional line is
    what sends a reader to grep by hand. Documenting that method's known failure anywhere else
    splits a prescription from its caveat.
  4. .github/workflows/** is not a PD-14 governed surface, and — measured — it publishes
    nothing. packages/spec/liveness is in @objectstack/spec's files[], so the ledger
    route would also have shipped this text into the npm tarball and taken a changeset.

Per the card's fence, exactly one site is written.

Class

Reviewer/tooling trap, not class (c). An author cannot reach the tombstoned key: the parse
door refuses it (the FAIL above is that door), and the ADR-0087 agent-tools-to-skills
conversion deletes it at rehydration. Nothing here is written up as a live authoring trap.

Verification

  • node scripts/docs-audit/check-drift-comment.mjs :: exit 0 — ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s). The new assertion pair runs on all 5 fixture cases,
    evidenced by the ablations below failing exactly 5 of the 66.
  • Ablation, both legs, mutation proven on disk and restored byte-exact via
    scripts/ablation-replace.mjs (anchor must hit; blob hashes compared against HEAD):
    • alter one censused name in the rendered line (joinsJOINS) — mutation landed
      (anchor 1 -> 0, blob f050c7c23948 -> 79f4b52e58d0), pin RED on all 5 cases, restore
      verified blob == HEAD (f050c7c23948) and git diff HEAD empty.
    • make the line conditional (if (false) limits.push(...)) — mutation landed, pin RED on
      all 5 cases, restored byte-exact.
    • Expected direction was "turns red", and that is what both legs produced.
  • Gate families derived from the real change set with
    node scripts/pm/dispatch-gates.mjs --commands (no hand-written path list): 47 derived,
    47 run
    , reconciled with --ran. 46 exit 0. pnpm check:pm-dispatch-gates is the
    checker's own repo-wide self-test suite and ran past two foreground budgets without
    reaching a verdict — recorded as NOT MEASURED, left to CI, never read as green.
  • Control-byte self-scan over both edited files beyond check:nul-bytes: no hits.
  • All readings taken at 549936fe47.

Acceptance notes

Out of scope for this PR, noted and not filed:

  • The census instrument is a one-shot measurement, not a shipped script. A census script with
    no gate reading it is the "code grep finds and the runtime never runs" shape from Route &
    surface ownership §1, and wiring a new gate would be a new verification surface this card
    does not authorize. The names are durable in the landed text and in the pin; the method is
    written down here. Successor: none — no open PR or queued card walks this file.
  • packages/spec/liveness/agent.json and packages/spec/liveness/README.md were not
    written
    , per the card's fence: both are held by open PR fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059 and both carry
    merge=os-regen, where a conflict resolves silently. Independently re-derived here over all
    32 open PRs (371 file rows; lit control: docs(spec): state what the map renderer actually does with an undeclared key in map #19102 holds exactly 3 files) — those two are the
    only held paths among this card's candidates.

Generated by Claude Code

…nd-grep remedy is prescribed

The advisory's fold already prescribes a hand re-read for the rule-carrying blind
spot. A hand re-read is a name-based search, and a name does not identify a key:
the same spelling is authorable on one governed type and a `[REMOVED]` tombstone
on another for `active`, `aria`, `joins`, `objects`, `template`, `tools` and
`version`. Nothing in a search result tells the two apart, so a grep hit on a live
example reads as evidence about the dead key — measured on #19059, where
content/docs/ai/agents.mdx was reported as contradicting the `agent.tools`
tombstone over an example inside a `defineSkill({` block.

The remedy's own failure mode now renders beside the remedy, unconditionally,
with the censused names as the payload and the discriminating parse as the fix.
Pinned byte-exact on every case in check-drift-comment.mjs, the same shape the
neighbouring unconditional line takes.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
@os-bill os-bill added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 18, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 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 07c6f822edd9c4c48a7ce34767de420c6096b9c3packageMentionDocs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants