Skip to content

test(mcp): pin that a missing recordId refusal names the id the caller sent - #17650

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-17568-update-record-key-mistake-refusal
Sep 11, 2026
Merged

os-sales merged 2 commits into
mainfrom
claude/issue-17568-update-record-key-mistake-refusal

Conversation

@claude

@claude claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #17568

Summary

Test-only. No production code changes, and that is load-bearing.

The card's premise is stale: the diagnostic it asks for is already correct on main. What was missing is anything holding it there. So this PR delivers the regression pin the card's acceptance criterion describes, and the empty production diff is itself the proof that the accepted argument set did not move.

Premise check — measured, not reasoned

The card reports that update_record with id: instead of recordId: answers:

MCP error -32602: Input validation error: Invalid arguments for tool update_record:
Invalid input: expected string, received undefined at recordId

Measured on this tree (packages/mcp at 76c9fab3, the branch point) the same call answers:

MCP error -32602: Input validation error: Invalid arguments for tool update_record: Invalid input: expected string, received undefined at recordId
Unrecognized key(s) on this update_record call: `id`. Did you mean `id` → `recordId`? This argument used to be dropped silently and the call still succeeded, so a mis-guessed parameter name answered a differently filtered or differently ordered set with no way for the caller to tell.

Both facts are already named — the missing recordId and the unexpected id that was sent, plus the mapping between them. The card was observed against a released service; #16913 has since closed every tool's argument shape with strictToolInput and given the record-scoped tools the alias table RECORD_ID_ALIASES (id, record_id mapping to recordId). Its changeset (.changeset/mcp-refuse-undeclared-tool-arguments.md, still pending release) carries the row for this exact pair. So the fix landed as a side effect of that sweep, and no message change is owed.

Both of the negatives the card asks for also already hold: a call carrying recordId succeeds and reaches the bridge unchanged, and a call carrying neither key gets the ordinary missing-recordId refusal with no unexpected-key clause invented for it.

Why a pin is still the right deliverable

Nothing pinned any of this, and the green is the intersection of three independent mechanisms — any one of which can regress while every existing gate stays green:

  1. zod reports both issues for a strict object — the unrecognized_keys for id and the invalid_type for the absent recordId. A schema that short-circuits on the first issue drops the half carrying the news.
  2. the MCP SDK's validateToolInput() joins every issue into one message. A change rendering only the first issue restores the card's defect verbatim.
  3. the alias table carries an id entry at all. It is one line, and it reads as decoration next to the tool it annotates.

The nearest sibling, mcp-http-tools.unknown-argument-keys.test.ts, does not cover this: every case there sends a complete set of valid arguments plus one probe key, so the unexpected-key half never has to coexist with a missing required key. The compound case is the one the card measured, and it was unguarded.

What is pinned — 10 cases

The named acceptance criterion, plus both negatives:

case asserts
the card's repro (objectName + id + data) refused; -32602 present; result.isError true with no protocol error; names recordId, quotes `id`, prescribes `recordId`; bridge untouched
negative 1a — recordId present succeeds, and the bridge receives exactly ('crm_lead', id, {status:'qualified'}) — not merely "did not refuse"
negative 1b — id present the bridge is never reached, so an alias cannot hide here
negative 2 — neither key ordinary missing-key refusal, asserted to quote no key at all
record_id, the other induced spelling same compound answer — shares no literal with the case above, so it reds if the seam is narrowed to a hard-coded id
get_record, delete_record inherit the compound refusal
run_action (where recordId is optional) names the sent id, and invents no missing-key complaint
create_record (declares no recordId) quotes id, prescribes nothing — the control on the prescription channel
the stdio transport the same refusal on a real StdioServerTransport

Two deliberate choices worth flagging to a reviewer:

  • Every key assertion uses the backtick form. A bare toContain('id') is satisfied by the word Invalid in Invalid input — it passes against the exact defect this file exists to catch. The negative is likewise asserted as "quotes no key at all" rather than "lacks some particular sentence", so it cannot be satisfied by a reworded invented clause.
  • Prose is not pinned, only named subjects — which keys the message names, and in which quoting position. The refusal pin's code + status half is asserted in this surface's own vocabulary: MCP answers a rejected tools/call as a tool error, so it is the protocol code -32602 plus isError: true with error absent, and HTTP 200 pinned so a later change cannot silently turn a tool error into a transport error.

Ablation — the pin fails, on the card's exact message

Reverting update_record's inputSchema to the pre-#16913 raw shape (zod's .strip then deletes id before the parse reports the absent recordId) reconstructs the defect. Run from the committed state; the mutation was proved on disk before the run (injected marker count 1, deleted anchor count 0, blob hash moved) and restored to a byte-identical blob afterwards (bf6d34d5b15f02a8e12a292653aa7b4918075a99 both before and after, clean git diff HEAD, zero marker residue):

× the reported `id` call is refused naming BOTH the missing `recordId` and the `id` it received
× `record_id` — the other induced spelling — is answered the same way
× names both facts on a real StdioServerTransport
Tests  3 failed | 7 passed (10)

AssertionError: expected '...' to contain '`id`'
Received: "MCP error -32602: Input validation error: Invalid arguments for tool update_record: Invalid input: expected string, received undefined at recordId"

That Received: line is the card's reported refusal, byte for byte. The 7 that stayed green are the negatives and the un-ablated sibling tools, which is the correct result.

Reach — measured per tool, never swept

update_record, get_record, delete_record all name both facts. run_action declares recordId optional, so nothing is missing and only the key mistake is named — the asymmetry is why reach is pinned per tool rather than asserted as a rule. create_record declares no recordId at all and correctly prescribes nothing. Both transports verified; the stdio half is asserted on the wire because this package has already shipped one outage in which the two transports served different surfaces while every per-surface pin stayed green.

Scope

The ruling on this card refused adding id as an alias for recordId (consumer-side tolerance that permanently widens a published MCP surface and teaches the caller nothing) and refused renaming recordId (a breaking change to a published tool parameter). Neither was needed and neither was done. The card's separately recorded second shape — all three parameters undefined — is out of scope and untouched, per the card's own instruction not to merge it with the above.

Gates

Derived from the final diff with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, each run with output redirected and $? captured before any pipe, then reconciled with --ran:

  • 51 derived, 48 green, 3 NOT MEASURED, 0 unrun. The three are check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt, each of which exited 3 printing PREREQUISITE NOT MET — they read built output across the whole workspace and need a full pnpm build, which is CI's run, not a finding in either direction.
  • check-keyed-text-bounds.mjs green; read by hit line rather than exit code, its population is *.object.ts declarations (112 files / 148 keyed text columns judged, 148 bounded) and it names no path in this diff. Moot twice over here, since no message text changes.
  • pnpm lint — the full union, not a narrowing: eslint . --no-inline-config --format json over 6620 files, 0 errors / 0 warnings, this file included and clean, measured at 9e33b0e0.
  • pnpm --filter @objectstack/mcp test — 30 files / 323 tests pass. pnpm --filter @objectstack/mcp typecheck green, including check:test-typecheck (existing ledger held; this file adds no entry and no error).
  • Coverage confirmed rather than assumed: tsc --project tsconfig.test.json --listFiles lists this file, so its assertions are compiled rather than phantom, and vitest list collects its 10 cases.

skip-changeset, measured

@objectstack/mcp publishes files: ["dist", "README.md", "CHANGELOG.md"], and tsup builds entry: ['src/index.ts'] only. After a real build of the package, with a positive control to prove the channel answers:

  • positive control — wireBridgeTools, a genuinely published symbol: 4 hits in dist/index.js
  • an existing test-only symbol (zzUndeclaredProbeKey, from the sibling test): 0 files in dist/
  • this file's own symbols: 0 files in dist/
  • test files emitted into dist/: 0

Nothing published moves, from any released package, so the label genuinely applies rather than being claimed.

Acceptance notes

  • The refusal renders as Did you mean \id` → `recordId`?, which leads with the caller's own spelling; the zod default sentence naming only recordId` also still comes first in the joined message. Both facts are present and the retry loop is broken, so this is not a defect under this card — noted, not filed, and deliberately not pinned, since the prose is not the contract.
  • The card's second shape (take v3, all three parameters undefined) is consistent with a client sending empty arguments and is not the same failure as guessing a parameter name; no change is recommended and none is made.

Generated by Claude Code

…aller sent

`update_record`'s parameter is `recordId`, which induces callers to send `id`;
the refusal used to answer `expected string, received undefined at recordId`,
naming the field the caller believes they already supplied. An agent reads that
as a value problem rather than a key problem and retries the same shape
forever. Measured on a real client across three recordings (5 of 7 call sites
sent `id` in one take).

The diagnostic is already correct on this tree: closing every tool's argument
shape with `strictToolInput` plus the `id`/`record_id` alias table made the
compound case answer both facts in one message. Nothing pinned it, and that
green is the intersection of three independent mechanisms -- zod emitting both
issues for a strict object, the SDK joining every issue into one message, and
the alias table carrying the entry at all -- any of which can regress while
every existing gate stays green. The nearest sibling pin sends a complete set
of valid arguments plus one probe key, so the unexpected-key half there never
coexists with a missing required key.

Test-only; no production change, so the accepted argument set provably does not
move. Both negatives are pinned beside the criterion: the declared spelling
still reaches the bridge with the exact arguments sent, `id` never reaches the
bridge at all, and a payload carrying NEITHER key still gets the ordinary
missing-key refusal with no key quoted in it.

Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Co-authored-by: Claude <noreply@anthropic.com>
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 11, 2026
@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
  • 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.

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

@os-sales
os-sales marked this pull request as ready for review September 11, 2026 08:42
@os-sales
os-sales enabled auto-merge September 11, 2026 08:42
@os-sales
os-sales added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit 9c9e6d0 Sep 11, 2026
35 checks passed
@os-sales
os-sales deleted the claude/issue-17568-update-record-key-mistake-refusal branch September 11, 2026 09:11
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…o the commits that decided them (objectstack-ai#20713)

Part of objectstack-ai#20594
Clause-②: no

## What changed

This is stage 7 of the `domain:cli` lane of the dead-citation sweep:
`packages/mcp/src`. Every comment site there that cited a tracker number
answering 404 now cites, in ruling C+D's form C (comment 5749154545 on
objectstack-ai#19123), the commit in this repository's history that decided what the
line describes, and keeps saying in its own words what that commit
decided. PR objectstack-ai#20533 is the method, and stages 1 to 6 of this card (PR
objectstack-ai#20624, PR objectstack-ai#20632, PR objectstack-ai#20656, PR objectstack-ai#20673, PR objectstack-ai#20689, PR objectstack-ai#20703) are the
precedents. The card stays open for the lane's remaining packages, so
this PR says `Part of`.

That is **17 sites on 17 lines in 9 files, covering 9 numbers**,
rewritten to **9 distinct commits**:
- the census's **10 sites**, in `mcp-server-runtime.ts` (5), `plugin.ts`
(3) and `stdio-data-bridge.ts` (2), 7 numbers;
- **7 test-file comment sites** in 6 test files (the census defers
`*.test.ts`; stages 1 to 6 took test comments too).

One more line changed: `__tests__/plugin-execution-context.test.ts:7`,
the second half of the `:6` sentence ("this face was not in that card's
inventory" now reads "not in that commit's inventory", since the card it
pointed back to is now named as a commit).

Only comments changed: **18 lines out, 18 in**, and every touched file
keeps its line count, so no line citation into these files moves. **No
citation number is added**: over the 18 line pairs, added-minus-removed
numbers is empty, and no PR number stands newly on any line. No ADR or
ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records any
of these 9 decisions (a grep for the 9 numbers there reads 0 hits, with
a control number from the same tree reading 2), so every anchor is a
commit.

**No changeset, and `skip-changeset`:** none of the rewritten comments
reaches `dist` (measured below: base and head emit six byte-identical
files, and a code-mutation control changes four of them). That is stage
5's case (PR objectstack-ai#20689), not stage 6's.

## Census: `packages/mcp`, before and after

**Instrument.** The gate's own `node scripts/check-issue-citations.mjs
--census --json`, read-only and unchanged, run under `with-fleet.sh
--read` for the token. The count is its `allocated-but-absent` findings
under `packages/mcp/`. Both runs enumerated the whole board.

| reading | tree | board | whole-repo `allocated-but-absent` |
`packages/mcp` sites | lines | numbers | files |
|---|---|---|---|---|---|---|---|
| before | base `e4e5222b7b`, run 2026-09-29T19:45:33Z to 19:50:37Z |
enumerated, 186 pages, frontier objectstack-ai#20708, 18,535 numbers | 1,222 | **10**
| 10 | 7 | 3 |
| after | `459ff81088`, run 19:58:39Z to 20:02:47Z | enumerated, 186
pages, frontier objectstack-ai#20709, 18,536 numbers | 1,212 | **0** | 0 | 0 | 0 |

The whole-repo drop of 10 is exactly these sites: a site-by-site diff of
the two JSON outputs has 10 findings gone, all under `packages/mcp/src`,
and none added. The other three tallies (`resolves` 32,968,
`resolves-as-pull-request` 1,984, `cross-repo-unjudged` 994) are equal
in both runs. `packages/mcp/src` is byte-identical at `459ff81088` and
at the head.

**Supplementary scan (test files included).** The gate's exported
`extractCitations` and `classifyCitation` over all 43 `.ts` files under
`src/`, with the board from the gate's own `probeBoard`: 365 citations
and 21 dead before (src comments 10, test comments 7, src strings 0,
test strings 4), 348 and 4 after (0, 0, 0, 4). Its before list of src
comment sites is identical to the census's. The 4 left are test titles,
the form-D stage (see Acceptance notes).

## Per-site table

`git blame` at the base ties each line to the commit that wrote it, and
each anchor was read in its message, changeset or diff, not only its
subject. Where the pull request that landed an anchor still answers, its
body's first line names the dead number, which is noted.

| number | sites (base line) | anchor: what it decided |
|---|---|---|
| `objectstack-ai#13318` | `mcp-server-runtime.ts:272` | `3ec8646f1`: the bridged
tools' `readOnlyHint` / `destructiveHint` come from what the definition
declares, and a tool that declares nothing is served neither hint
(omit-when-unsourced). The line blames to `c39369d12`, the
`openWorldHint` sibling, whose changeset calls this the repair "that
preceded it". The PR that landed `3ec8646f1` answers 404 too. |
| `objectstack-ai#6724` | `mcp-server-runtime.ts:625`;
`mcp-server-runtime.metadata-outage.test.ts:289` | `4f3d2322e`: corrects
`diagnoseEmptyRead`'s falsified claim that `MetadataFacade.getObject`
differs from `get('object', n)`, in the TSDoc and in the outage test's
restatement of it. Both lines blame to it; PR objectstack-ai#6948, which landed it,
names objectstack-ai#6724. |
| `objectstack-ai#6745` | `mcp-server-runtime.ts:636` | `7a5ef0008`: adds
`metadata-service-getobject-equivalence.test.ts`, pinning `getObject(n)`
equal to `get('object', n)` across all three implementations. The line's
"PR objectstack-ai#6839 for objectstack-ai#6745" named this commit's PR (answers 200), which stays
beside the sha as a convenience link. The spec lane gave the number this
anchor. |
| `objectstack-ai#6723` | `mcp-server-runtime.ts:637`, `:652`;
`mcp-server-runtime.metadata-outage.test.ts:293` | `8ad609c69`: declares
on `IMetadataService.getObject` that it answers the same as
`get('object', name)`. `objectstack-ai#6723` was the pull request that landed as this
commit (its subject carries the number); `objectstack-ai#6505`, the issue beside it on
`:637`, answers 200 and stays. The spec lane gave the number this
anchor. |
| `objectstack-ai#17114` | `plugin.ts:8`, `:67`;
`stdio-tenancy-posture-api-key-matrix.test.ts:569` | `4af758d47`: the
last two admission doors, this one included, classify the tenancy
rejection through the shared `classifyAdmissionTenancyPosture`. All
three lines blame to it; PR objectstack-ai#17683 names objectstack-ai#17114, and stage 1 gave the
number this anchor. |
| `objectstack-ai#6216` | `plugin.ts:126`;
`__tests__/plugin-execution-context.test.ts:6` | `f586f1a89`: one
`ExecutionContext` assembler for the dispatcher, REST and share-link
sites. Both lines blame to `502dc6fe7`, which converged this stdio face
afterwards and names that convergence as its precedent. Its file list
touches no `packages/mcp` file, which is what `:7` ("not in that
commit's inventory") says. Stages 1 and 2 and the spec lane gave the
number this anchor. |
| `objectstack-ai#8422` | `stdio-data-bridge.ts:85`, `:394`;
`stdio-data-bridge.not-found.test.ts:4` | `4810dd628`: the stdio
bridge's by-id write seams throw the shared `recordNotFoundError`
envelope instead of a bare `Error`. All three lines blame to it; PR
objectstack-ai#8507 names objectstack-ai#8422. |
| `objectstack-ai#17568` | `mcp-record-id-key-mistake-refusal.test.ts:4` |
`9c9e6d08f`: pins that a missing-`recordId` refusal also names the `id`
the caller sent (test-only). The line blames to it; PR objectstack-ai#17650 names
objectstack-ai#17568. |
| `objectstack-ai#13486` | `mcp-tool-bridge-safety-annotations.test.ts:423` |
`6193e576d`: pins the bridge's two hand-copied safety name sets in the
direction the old pin could not see (the docblock's heading is that
commit's subject). The line blames to it; PR objectstack-ai#13888 names objectstack-ai#13486. |

**Anchor checks.** Every cited sha matches exactly one object (`git
rev-parse --disambiguate`, count 1 for each of the 9), is a commit, has
one parent, and is an ancestor of `main` (`merge-base --is-ancestor`
against `5757463712`, exit 0 for all 9). The checkout is not shallow.
The control leg `979ad9575` (2026-08-08, the parent of the oldest anchor
`8ad609c69` of 2026-08-08) exits 0, and the negative control, this
branch's own `459ff81088`, exits 1. Four anchors reuse the landed
stages' (`f586f1a89`, `4af758d47`, `7a5ef0008`, `8ad609c69`), so each
number carries one anchor across the tree; five are new (`3ec8646f1`,
`4f3d2322e`, `4810dd628`, `9c9e6d08f`, `6193e576d`).

**Numbers.** All 9 dropped numbers answer 404 by REST (re-probed
2026-09-29T19:54Z). The numbers kept on changed lines (`objectstack-ai#6839`, a pull
request; `objectstack-ai#6505`, `objectstack-ai#15348`, `objectstack-ai#16013`, `objectstack-ai#4435`, `objectstack-ai#5138`, `objectstack-ai#7867`) answer
200. Four slash-joined groups stand in `packages/mcp/src`, whose later
halves the citation grammar does not read (`objectstack-ai#4435/objectstack-ai#5138/objectstack-ai#7867` twice,
`objectstack-ai#5138/objectstack-ai#5581`, `objectstack-ai#7728/objectstack-ai#7823`); every half answers 200, so none is dead.

## Mechanical guard: no code token moves

**H2 holds on both readings: the parser leaf-token diff is empty, and
the emitted `dist` is byte-identical.**

**Token guard.** It compares the TypeScript parser's leaf tokens
(TypeScript 6.0.3, JSDoc nodes excluded) of the 9 touched files at base
`e4e5222b7b` and at `459ff81088`. Controls mutate the head text in
memory only.
- Real run: 21,192 base tokens, 0 differing (exit 0).
- Comment-insertion control: 0 differing (exit 0).
- Code-insertion control: all 9 files differ at token 0 (exit 1).
- String control (the first character of the `'vitest'` import specifier
in `plugin-execution-context.test.ts` flipped): exactly 1 differing
`StringLiteral`, at token 15 of that file (exit 1).

**Emitted `dist`.** `pnpm --filter @objectstack/mcp build` at the head,
then at base (the base tree of `packages/mcp/src` restored in place
under a trap-armed restore; an on-disk probe read `objectstack-ai#13318` 1 and `commit
3ec8646` 0 before that build; afterwards every touched blob equals its
HEAD blob and `git diff HEAD` is empty), with the same dependency
builds:
- all six files (`index.cjs`, `index.cjs.map`, `index.d.cts`,
`index.d.ts`, `index.js`, `index.js.map`) are **byte-identical** by
sha256. The built files do carry docblocks (14 in `index.js`, 78 in
`index.d.ts`); none of the rewritten ones is on an emitted declaration.
- Code-mutation control (`scripts/ablation-replace.mjs`, anchor: the
sync leg's typed `ctx.getService` call on `'tenancy'` in `plugin.ts`,
hit 1 to 0, its argument renamed to a marker; blob restored to HEAD
`0a1aaa7955`, `git diff HEAD` empty):
`scripts/ablation-dist-preflight.mjs` found the marker in `index.cjs`
and `index.js`, and `index.cjs`, `index.js` and both `.map` files differ
from the head build. `dist` was then rebuilt, its six sha256 values
equal the first head build, and the preflight in `--absent` mode reads
the marker absent from all 6 files with a clean tree.

A raw scan of the 9 changed files for control bytes finds none (a
positive probe on a scratch file matched).

## Changeset

**None, and `skip-changeset`.** `@objectstack/mcp`'s `files[]` is
`dist`, `README.md` and `CHANGELOG.md`, and the build above emits
byte-identical `dist` at base and head, so this diff publishes nothing
from any released package. Stage 5 (PR objectstack-ai#20689) measured the same and
shipped the same; stage 6 (PR objectstack-ai#20703) measured the opposite and carried
a `patch`.

## Gates (head `7a0f15de62`)

This host has no `flock`, so `os-verify-lock.sh` ran in its declared
unlocked mode. Its disclosure, verbatim, from each run at this head and
from the four `dist` builds (at `459ff81088`, `packages/mcp/src`
byte-identical to this head):

```text
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 25s · declare it in the PR body · pnpm --workspace-concurrency=2 --filter '@objectstack/mcp...' build
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 116s (1m56s) · declare it in the PR body · pnpm turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 10s · declare it in the PR body · pnpm --filter @objectstack/mcp exec vitest run --maxWorkers=2
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 4s · declare it in the PR body · pnpm --filter @objectstack/mcp typecheck
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 3s · declare it in the PR body · pnpm --filter @objectstack/mcp build
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 3s · declare it in the PR body · pnpm --filter @objectstack/mcp build
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 3s · declare it in the PR body · pnpm --filter @objectstack/mcp build
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 2s · declare it in the PR body · pnpm --filter @objectstack/mcp build
```

- **Build:** `@objectstack/mcp` with its closure (9 of 81 workspace
projects), then the whole workspace, `turbo run build
--filter='./packages/*' --filter='./packages/*/*'`, 71 of 71 tasks,
after the merge. The tree was clean after both.
- **Tests:** `vitest run`: 32 files, 344 tests passed (every `*.test.ts`
under `src/`), at the head and before the merge.
- **Typecheck:** `pnpm --filter @objectstack/mcp typecheck` exits 0.
`tsc --listFiles`: `tsconfig.json` compiles the 11 non-test `src` files,
`tsconfig.test.json` all 43 including the 32 test files.
`check:test-typecheck`: 6 files, 53 errors, 8 pinned signatures, held.
- **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`)
exits 0 at this head (2026-09-29T20:18:54Z to 20:19:23Z), and at
`459ff81088` before the merge.
- **Citation judging:** after merging `origin/main` (`9b384f63ae`),
`node scripts/check-issue-citations.mjs --base 9b384f6` judges 5
citations on the changed lines of 3 files (the kept numbers `objectstack-ai#15348`,
`objectstack-ai#16013`, `objectstack-ai#4435`, `objectstack-ai#6505`, and `objectstack-ai#6839` as a pull request) and exits 0:
every one resolves. Against `origin/main` after it moved to
`5757463712`, the same 5 citations, exit 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 53 families. All 53 exit
0, and `--ran` with the exit-coded record reads "53 derived, 53 run, 0
NOT-MEASURED, 0 UNRUN" (a derived zero). Among them:
`check:issue-citations`, `check:doc-authoring` (808 pinned sites, no
growth), `check:nul-bytes` (9,331 files, no raw control bytes),
`check:published-files`, `check:type-check-debt`.
- **Artifact rosters:** 36 of the 39 non-self-test roster rows exit 0,
including the three the derivation marks as keeping their roster under
one of this diff's paths (`check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity`). The other three
need a pull request's context; they are run against this PR once it
exists and reported on the card. The 18 self-test-only rows grade their
checkers' fixtures and cannot judge this diff.

## Hypotheses (measured first)

- **H0 holds.** At base `e4e5222b7b` the filtered census answers 10
sites on 10 lines, 7 numbers, in 3 files, as on the seat's `0be898499f`.
The whole-repo count is 1,222.
- **H1 holds.** After the rewrite, the filtered census answers 0 for
`packages/mcp`. No site was left for an open PR (the file lists of all 8
open PRs were read at 20:08:05Z: only the Version Packages PR objectstack-ai#20639
touches `packages/mcp`, in `CHANGELOG.md` and `package.json`) or for an
unfound anchor.
- **H2 holds, on both readings.** The comment-stripped (parser-token)
diff of all 9 touched files is empty with its controls firing, and the
emitted `dist` is byte-identical at base and head with a code control
that changes it.

## Acceptance notes

- **Test titles, the form-D stage.** 4 dead numbers remain in test
string literals in `packages/mcp/src` (`describe` titles, no assertion
text): `objectstack-ai#17568` twice in `mcp-record-id-key-mistake-refusal.test.ts`
(`:151`, `:315`), `objectstack-ai#8422` in `stdio-data-bridge.not-found.test.ts:99`,
`objectstack-ai#17114` in `stdio-tenancy-posture-api-key-matrix.test.ts:592`. They
stay on the card for its form-D stage; no string moved here.
- **Outside `src/**`, a later stage of the card:**
`packages/mcp/vitest.config.ts:18` cites `objectstack-ai#8651` (404).
`packages/mcp/test-typecheck-debt.json:2` cites `objectstack-ai#13470` (404) inside
its `_comment` field, which the file itself says is generated by
`scripts/check-test-typecheck.mts`, so a fix there is at that producer,
in the `scripts/**` lane, not a hand edit. The other citations in
`packages/mcp` outside `src/**` (`CHANGELOG.md` excluded) answer 200.
- **Card-word residue, cited nowhere.** A few docblocks still say "this
card" or "the card" a paragraph away from the rewritten line (for
example `stdio-data-bridge.not-found.test.ts:19`,
`mcp-record-id-key-mistake-refusal.test.ts:19`,
`stdio-tenancy-posture-api-key-matrix.test.ts:580`, `:584`). They cite
no number, so they were left, as the landed stages left theirs; only the
one same-sentence companion (`plugin-execution-context.test.ts:7`) was
changed.
- **The moving `origin/main`.** The branch merged `origin/main` once
(`7a0f15de62`, merging `9b384f63ae`: `service-storage`,
`platform-objects` and `plugin-audit`, nothing in `packages/mcp`). A
later fetch advanced the shared ref to `5757463712`, one commit in
`platform-objects` translations. There was no second merge; CI judges
the merge ref.

## Deviations

- **One companion line (`plugin-execution-context.test.ts:7`)** beyond
the 17 sites, the second half of the `:6` sentence.
- **Commit trailers** are AGENTS.md's model-free pair (`Claude-Session`
plus `Co-authored-by: Claude`), and the pre-push trailer check passed on
every push. The harness's attribution reminder asked for a model-named
trailer and a different PR footer, and AGENTS.md overrides it. The merge
commit carries git's default message.

---
_Generated by [Claude
Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_

Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants