Repository navigation
Commit b7eaf6a
ci(docs-drift): state the same-name-different-shape trap where the hand-grep remedy is prescribed (#19110)
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 #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 #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 (`joins` → `JOINS`) —
mutation landed
(`anchor 1 -> 0, blob f050c7c -> 79f4b52e58d0`), pin **RED** on all
5 cases, restore
verified `blob == HEAD (f050c7c)` 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 #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: #19102 holds exactly 3 files) —
those two are the
only held paths among this card's candidates.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 1124c45 commit b7eaf6a
2 files changed
Lines changed: 81 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
355 | 355 | | |
356 | 356 | | |
357 | 357 | | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
358 | 401 | | |
359 | 402 | | |
360 | 403 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
230 | 230 | | |
231 | 231 | | |
232 | 232 | | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
233 | 264 | | |
234 | 265 | | |
235 | 266 | | |
| |||
348 | 379 | | |
349 | 380 | | |
350 | 381 | | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
351 | 389 | | |
352 | 390 | | |
353 | 391 | | |
| |||
0 commit comments