Skip to content

feat(client, rest): bind both getHistory exits to HistoryMetaItemResponse; ledger row names the schema - #16694

Merged
os-sales merged 3 commits into
mainfrom
claude/issue-13523-history-door-schema-rebind
Sep 7, 2026
Merged

os-sales merged 3 commits into
mainfrom
claude/issue-13523-history-door-schema-rebind

Conversation

@os-sales

@os-sales os-sales commented Sep 7, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #13523

Clause-②: no

Clause-② — ruled yes by the dispatching PM (#6024), on the condition its own Claim: set

The claim (5574952927) declared Clause-② conditionally: yes if the hand-written inline shape and z.infer<typeof HistoryMetaItemResponseSchema> differ, no if they are byte-equivalent. The delivered changeset measures them as differing, so the condition resolves yes:

member inline shape (old) published HistoryMetaItemResponse
actor string nullable — the door answers null for every system-initiated write (boot sync, migration, scheduled job), and the schema declares it "never a sentinel string"
op plain string the closed ADR-0008 §2.4 verb union (create/update/delete/rename/publish/revert)
version, previousName, ref.version absent reachable
ref.org org?: string (optional) required — what the producer always writes
the scoped twin no annotation, _unwrap with no type argument ⇒ Promise<unknown> the same named type as the unscoped exit

⇒ the published return type narrows. Same shape as #14313, and the maintainer's #12104 ruling (5472614711) puts this class at Clause-②: yes explicitly. The changeset is graded @objectstack/client: minor / @objectstack/rest: patch and carries BREAKING (types) in its own body.

The two consumers bound

  1. Route-ledger row — packages/rest/src/rest-route-ledger.ts, the GET /api/v1/meta/:type/:name/history entry now carries responseSchema: 'HistoryMetaItemResponseSchema', joining its already-bound audit / rollback / diff siblings. Data only, in a package-internal module: no route, handler or emitted byte moves. This row was the one member of the metadata family left unfilled, because the declaration (MetadataProtocol declares no historyMetaItem member, so the REST history door's request literal is compiled against nothing — the last undeclared read door of the audit/history pair #12005, PR feat(spec): declare MetadataProtocol.historyMetaItem and de-cast the REST history door #13521) landed after the row was written.
  2. Both getHistory exits in packages/client/src/index.ts — the unscoped ObjectStackClient.meta.getHistory and the scoped ScopedEnvironmentClient.meta.getHistory, each now annotated Promise<HistoryMetaItemResponse> with the unwrap call given the matching type argument.

The two getHistory occurrences: measured, not assumed

Triage flagged binding only one as "this card's most likely error" and asked for the relationship to be measured. They are two exits of ONE door, not two doors.

Located by symbol (⛔ never by the stale line numbers — the file moved under #15445, #14526 and #16676): packages/client/src/index.ts holds exactly two getHistory definitions, one on ObjectStackClient.meta and one on ScopedEnvironmentClient.meta.

The producer side proves they are one mount. In packages/rest/src/rest-server.ts, registerMetadataEndpoints(bp) is called from inside the registerForBase(bp) closure, and registerForBase is invoked twice — once with basePath and once with scopedBase (${basePath}/environments/:environmentId). Inside it, isScoped is derived from that same string (basePath.includes('/environments/:environmentId')) and registerPerItemRoute registers one handler body for the /:type/:name/history path under both bases; the only difference is whether environmentId is read from req.params. ⇒ one handler, replayed against two prefixes, answering a byte-identical body.

Both exits are bound. Their declared states before this change were nevertheless different — the unscoped carried a hand-written inline object type, the scoped carried nothing (Promise<unknown>) — so binding one and leaving the other would have relocated the divergence rather than removed it. return-type-precision.test.ts pins UnscopedHistory ≡ HistoryMetaItemResponse, ScopedHistory ≡ HistoryMetaItemResponse and UnscopedHistory ≡ ScopedHistory; that third assertion is the one that refuses a half-fix, and the ablation below shows it is red for either half alone.

Gates — measured at head cd9f93413eb577817fdfa9d0ad8cb7867dcd079b

Every reading below is against that commit, working tree clean (git status --porcelain empty, git diff HEAD empty). Each exit code was captured before any pipe; no verdict is read from a ;-sequenced batch.

origin/main moved to 001a83b048 during this run. It was not re-merged: its one new commit (#16650) touches packages/drivers/*, packages/services/*, packages/types/* and adds two new test files under packages/rest/src/ — it overlaps none of this PR's five files, so no merge was needed and the head above is the head that was measured.

Measured green

gate result
dispatch-gates --commands derived family — 58 commands, all run individually 58/58 exit 0
dispatch-gates --ran ran.list reconciliation ✓ 58 derived famil(ies) accounted for — 58 run, 0 NOT-MEASURED, 0 UNRUN
pnpm --filter @objectstack/client typecheck exit 0 — tsc --noEmit clean; check:test-typecheck OK, 0 files / 0 errors in test-typecheck-debt.json
pnpm --filter @objectstack/rest typecheck exit 0 — same, 0 files / 0 errors
@objectstack/client unit tier (pnpm --filter @objectstack/client test) exit 0 — 35 files, 454 tests passed
the two touched test files run explicitly exit 0 — 2 files, 20 tests passed
pnpm exec eslint . --no-inline-config (whole repo) exit 0, zero output

The ran.list was built from the byte-for-byte command strings --commands printed (verified diff-identical to the harvested list), never from log file names.

NOT MEASURED — with each gate's own stated prerequisite

⚠️ dispatch-gates --ran's 0 NOT-MEASURED is the runner's own accounting of the derived set, not verification of everything CI runs. The honest split:

  • check-changeset-no-major LEVEL AXIS — exit 0 overall, but the gate itself prints: "LEVEL AXIS: NOT MEASURED — no clause-② declaration was readable for this PR… no pull_request payload was available to read a declaration from." Prerequisite: a live pull_request payload, which exists only once this PR does. CI measures it on this PR.
  • 5 families that take a value from the workflow — check-shard-attestation.mjs (×3, ${{ matrix.shard }}) and check-test-completeness.mjs (×2, $RUNNER_TEMP/*.log). Prerequisite: a CI run; there is no local invocation. Named by the tool as ⊘ NOT MEASURED and deliberately outside the 58.
  • 10 families declaring a population too wide to place — check:init-service-contract, check:live-db-isolation, check:meta-type-normalized, check:optional-error-sink, check:resume-authority-declared, check:runner-env-posture, check:settings-bind-window, check:startup-registry-verdict, check:verify-stand-in, check:wildcard-fallthrough. lint.yml carries no paths filter, so CI runs them on every PR; no path derivation can say whether they bite. Their absence from the derived total is not a clearance.
  • 41 artifact-roster families scored silent — a fact about a tracked-file baseline, not about these paths (4 of them keep that roster in a directory one of these paths is in). Not clearance in either direction.
  • The always-runs step tail (workflow steps with no path filter, e.g. pr-labels.mjs, the publish smoke script) — outside --commands by construction, run by CI.

Zero gates hit exit 3, zero were killed by the OS, zero fell to UNRUN.

Ablation — the new pin is proved able to fail

return-type-precision.test.ts is the guard for this whole card, so it was ablated rather than trusted. Nothing was committed: the committed tree was mutated in place by exact-anchor replacement that refuses on anchor count ≠ 1, the mutation was proved on disk by occurrence counts and git hash-object, and restoration was proved by blob hash and an empty git diff HEAD — never by an exit code. A trap … EXIT INT TERM with absolute paths held the restore.

Pristine blob of packages/client/src/index.ts: 7fa9e129ca1aa66c8bb099a243351e9b73389606.

row mutation (exact anchors, each count = 1) mutated blob on-disk proof source tsc --noEmit pin program check:test-typecheck vitest on the pin file
A unscoped exit only → the OLD inline return shape + untyped unwrapResponse(res) 9e7640b5ee… Promise<HistoryMetaItemResponse> 2→1; old actor: string; 0→1; unwrapResponse<…> 1→0 exit 0 (clean) exit 1 — RED, 9 type errors in src/return-type-precision.test.ts exit 0
B scoped exit only → no annotation + untyped _unwrap(res) 12e1014d8f… Promise<HistoryMetaItemResponse> 2→1; _unwrap<…> 1→0 exit 0 (clean) exit 1 — RED, 3 type errors in src/return-type-precision.test.ts exit 0
A+B both exits reverted (the pre-change state) 615a6ac70b… Promise<HistoryMetaItemResponse> 2→0; both unwrap type args gone exit 1 — TS6133 'HistoryMetaItemResponse' is declared but its value is never read (a side effect of the full revert, not the pin) exit 1 — RED, 11 type errors in src/return-type-precision.test.ts exit 0
restore — back to 7fa9e129ca… — — — —

Restoration after every row: blob hash matched pristine and git diff HEAD was empty. Final state: git status --porcelain 0 lines, HEAD still cd9f93413e, git hash-object packages/client/src/index.ts = 7fa9e129ca….

Two readings worth stating plainly:

  • Rows A and B are the half-fix test, and both are red. Binding one exit and leaving the other does not get past this pin. That is precisely the error triage warned this card was most likely to make.
  • vitest is green in every row, and that is correct, not a hole. The pin is type-level on purpose: expectTypeOf and @ts-expect-error are erased at runtime, so the value the stubbed transport returns is identical either way. The teeth are in check:test-typecheck (which compiles the test layer under packages/client/tsconfig.test.json), and the pin file's own docblock says so. A runtime-only reading of this pin would have reported a false green three times.

Serial scan — packages/client/src/index.ts is clear

Re-scanned at claim time as the card demands, ⛔ not taken from any comment. All 17 open PRs enumerated and paged to the last page (list_pull_requests state=open perPage=100; page 1 returned 17, page 2 returned empty — the paging failure that cost this card two days is exactly what that second call exists to rule out), then each scanned per ref: git fetch --depth=80 origin refs/pull/N/head, git merge-base FETCH_HEAD origin/main, git diff --name-only <merge-base> FETCH_HEAD.

Result: every one of the 17 refs fetched and resolved a real merge base within depth; none holds packages/client/src/index.ts. The only open PR touching packages/client/ at all is the changeset-release bot (#15334), and only CHANGELOG.md / package.json — same-package, which is exempt in this lane; same-file is the hard-serial rule and nothing holds it. The four cards historically queued on this file (#14313, #14314, #15451, #14879) have no open PR: #14879's carrier PR #16676 merged at 19:32Z, and none of the 17 open heads is any of the others.

Method validated rather than assumed: the git per-ref file list for the largest open PR (#16628, 37 files) is byte-identical to GitHub's own get_files list for it.

Scope

⛔ Out of scope, preserved from the card: the dead environmentId wire-spread on the same door belongs to #12005, not this card. Untouched here.

No wire byte moves. HistoryMetaItemResponseSchema is a describe-only transcription of what historyMetaItem already returned, and the SDK's runtime path is unchanged — only what the compiler knows about it.


Generated by Claude Code


Generated by Claude Code

…onse; ledger row names the schema

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
The `note` field ships to authors and operators through generated
surfaces, where an issue id resolves to nothing. The anchors move to an
adjacent source comment, which only a reader who can resolve them sees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
@github-actions github-actions Bot added the size/m label Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/client, @objectstack/rest, touching 6 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/environment-routing.mdx (via /environments/:environmentId (route, a path literal in meta))
  • content/docs/concepts/north-star.mdx (via /environments/:environmentId (route, a path literal in meta))
  • content/docs/deployment/publish-and-preview.mdx (via /environments/:environmentId (route, a path literal in meta))
  • content/docs/deployment/single-project-mode.mdx (via /environments/:environmentId (route, a path literal in meta))
  • content/docs/kernel/contracts/metadata-service.mdx (via getHistory (sdk, the bare tail of client method meta.getHistory, bound to GET /api/v1/meta/:type/:name/history))
  • content/docs/plugins/adding-a-metadata-type.mdx (via /meta/:type/:name/history (route, a path literal on a changed line))
  • content/docs/protocol/kernel/http-protocol.mdx (via /environments/:environmentId (route, a path literal in meta))
  • content/docs/protocol/kernel/metadata-service.mdx (via /environments/:environmentId (route, a path literal in meta))
  • content/docs/ui/forms.mdx (via /environments/:environmentId (route, a path literal in meta))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via /environments/:environmentId (route, a path literal in meta))

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
  • 1 cross-cutting symbol(s) contributed no route anchor: responseSchema (6 routes)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 22 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 001a83b0486391847d45f6866896c53ad8714569 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 001a83b0486391847d45f6866896c53ad8714569

⚠️ 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 001a83b0486391847d45f6866896c53ad8714569 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 7, 2026
@os-sales os-sales added needs:contract-review and removed documentation Improvements or additions to documentation tests tooling labels Sep 7, 2026 — with Claude

os-sales commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Check Changeset 红,已定位;⛔ 本席暂不推修,因为正确的修法取决于一小时内会落定的裁定

domain:cli 执行 PM 席(#6024),R70。头 cd9f93413e。这是本席自己开的 PR,按 drive-to-green 本条即是「说清楚什么在挡、需要什么」的那一条评论。

失败读数(读的是 job log,不是事件摘要)

✓ This diff introduces no `major` bump.
⛔ This PR declares clause-② YES and grades a package it grew `patch`.

   .changeset/history-door-schema-rebind.md
     - @objectstack/rest: patch   ← this PR moves @objectstack/rest's packages/*/src/**

The two declarations disagree, inside one PR:
   · carrier: `needs:contract-review` IS on this PR
   · declaration line: `Clause-②: yes`

⇒ ⚠️ 点名的不是 @objectstack/client(它是 minor,没问题),是 @objectstack/rest。触发的原因是本 PR 动了 packages/rest/src/rest-route-ledger.ts —— 落在 packages/*/src/** 之下,于是档位轴判定该包「长过」。

⛔ 五条红是同一次失败的回声(PR 开启 + 本席两次挂标各触发一次重跑),不是五个问题。⛔ 不是 flake,不需要 re-run:它是确定性的,同一棵树上每次都红。

为什么现在不推修

闸自己给了两条路,并写明二者不可互换:

  1. 声明对、档位错 → 抬到 minor。
  2. 档位对、声明错 → 在生产者处改正。
    ⛔ Do not add a tolerance here to route around a declaration that says something its author did not mean.

⇒ 走哪一条,取决于 Clause-② 究竟是不是 yes —— 而那正是此刻达档(fable)复核在裁的东西,本席已明确授权它判 no。

  • 若复核确认 yes ⇒ 走第 1 条:把 @objectstack/rest 抬到 minor。
  • 若复核判 no ⇒ 两侧载体一笔剥除、改判 Claim:,耦合随之消失,changeset 一个字都不用动,闸自绿。

⚠️ 此刻就把 rest 抬到 minor,是在一个可能一小时内被推翻的声明上,对发布做一次真实改动;若随后被推翻,还要再回退一次。⇒ 等裁定是更省的一步,而不是拖延。⛔ 本席也不会去加容差绕开它——闸把这条明写为禁止。

⭐ 顺带:这是同一条判据今天第二次咬本席,而且这次是反方向

今早 #16672 上,Clause-②: yes + patch 被这条耦合判红,那次是对的。

而这一次值得记下来的是判据的形状:rest-route-ledger.ts 是一个包内模块里的数据行 —— changeset 自己写着「no route, handler or emitted byte changes」。它没有长出任何公开面。但因为它住在 packages/rest/src/** 之下,档位轴就把它算作「长过」。

⚠️ 而本席今晚早些时候立的 #16692 记的是同一条判据的另一头:judgeLevel 只经 packages/*/src/** 认定「长过」,所以一个出货的 bin/ 目标它看不见。

⇒ 合起来看:同一个路径判据既漏判出货面、又误判内部数据行。已把这个反方向的读数补进 #16692。⛔ 本 PR 不改闸、不加容差 —— 那是 #16692 的事,不是这张卡的。


Generated by Claude Code

os-sales commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

契约复核 PASS — Clause-②: no,推翻本席的 yes;载体已清,正文声明已改写

domain:cli 执行 PM 席(#6024),R70。Reviewed-by: 达档契约复核(CONTRACT_REVIEW_TIER,claude-fable-5-1),头 cd9f93413e。整份采纳。改判 Claim: 见 #13523 (comment)

$ node scripts/pm/check-clause2-carriers.mjs --pair 16694
✓ PR #16694 / card #13523 — both carriers agree.    EXIT=0

⛔ 先记本席在执行上的一次失手,而不是等它被别人发现

本席把「一笔」做成了半笔。 先摘了卡片侧的载体,然后转去改 PR 正文,忘了摘 PR 侧 —— 于是 check-clause2-carriers 立刻判 exit 4:

✗ C1 — needs:contract-review on delivering open PR #16694 while card #13523 does NOT carry it — the same split, written from the other end.

⇒ 这正是双载体纪律存在的目的所要防的那种劈裂,而制造它的是本席。⚠️ 并且按闸自己的话:

a legitimate clear leaves two removals seconds apart, a strip leaves one

本席这两次摘除相隔的是分钟,不是秒。⇒ 证据链因此变弱了一档:它仍然可读为「清」,但不再是那个一眼可辨的形状。⛔ 记在这里,不粉饰。

裁定要点(复核的,非本席的)

两支都不触发。

  • 接受/拒绝支:handler 未动,rest-server.ts:7462 裸发 res.json(result);HistoryMetaItemResponseSchema 在 packages/rest/src 与 packages/client/src 里运行时零处 parse;台账的 responseSchema 够不到任何运行时或发现面 —— REST_ROUTE_LEDGER 根本未从 packages/rest/src/index.ts 导出。
    ⭐ 归谬:编译器对消费者代码的判决不是契约的接受/拒绝行为。若那样读,每一次 .d.ts 更正都是条款②,包括 ADR-0087 D8 专为「使之成为常规」而写的 any→具体 收窄。
  • 公开面支:无新导出符号,已发布载荷无新键,wire body 逐字节不变,其已发布声明(protocol.zod.ts:1567)未动。移动的是 SDK 注解 —— 从一个与已声明契约相矛盾的手写形状(actor: string,而 rowToEvent 写 null),回到那份已声明的契约本身。⇒ SKILL.md:393 后半句逐字:「拉回已声明契约 ⇒ 代裁车道」。

同形先例 PR #12848(#12038 交付):16 个方法、18 条台账行,完全相同的动作,无内容支声明、无载体,@objectstack/rest 定 patch。

⭐ 本席错在援引越界,而非读错判据

本席称维护者 #12104 把这一类明写为 yes。复核读了原文:那句在 「执行形状……按族三张卡 —— organizations 19 / auth 14 / oauth 5……每卡 Clause-② yes」 之内 —— 「每卡」指那三张。且那三张另有一个与括号无关的独立理由:它们迁到新撰写并导出的 wire 类型,直接命中机械地板的「新导出符号」。本卡不撰写契约、不导出任何东西。

⇒ 判 no 不推翻任何东西。而复核给了本席一条更该记住的程序:

若 PM 认为维护者「本意」是类别规则,正确的动作是提 needs-user-decision,⛔ 不是靠外推去判 yes。

⚠️ 这是本席今天第三次条款②过火,且在前两次已被纠正之后。

档位:一个字不改

@objectstack/client: minor / @objectstack/rest: patch 均正确。minor 既是地板也是天花板 —— check-changeset-no-major.mjs:41-45 在发布窗口内机械拒绝 major。rest: patch 与 #12848 同判。

⇒ 那条红自绿:judgeLevel 在 declaration.value === 'no' 时返回 not-declared(:940)。⛔ 本席未抬档、未加容差 —— 走的是闸自己给的第 2 条路(声明错,在生产者处改正),而非第 1 条。

⚠️ 正文的 Clause-②: 行必须同笔改写,因为闸读的是 readClause2Line(pr.body) 加标签快照(:902);只摘标签,闸仍见不一致。已改,读回确认 line 3 "Clause-②: no"。

⚠️ 副作用,已测且不再尝试修

本席经直接 REST PATCH 改正文,平台每次都追加自己的裸脚注 —— 送进 1 个脚注、读回 2 个;再送 1 个、仍读回 2 个,字节数相同。⇒ 重复脚注是通道属性,不是可以靠再改一次消掉的东西。⛔ 停手,不做第三次 PATCH。读数已转记到 #16633(该卡记的正是「同一动作经 MCP 通道不追加」的另一半)。

复核的建议(均不阻塞,已转承接者)

F1 previousName 在这扇门上从未被写入(rowToEvent 不设它)⇒ changeset 不应暗示它被填充;F2 台账注记说 conformance 捕获「carrying every optional member」,而 protocol.test.ts:2166 漏了两个;F3 应写 z.input<> 而非 z.infer<>(ADR-0122);F4 本席措辞不准 —— 破坏性的 actor 是放宽(string→string | null),不是收窄。


Generated by Claude Code

os-sales commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Docs-drift owed item: all 10 rows answered — nothing falsified, nothing pushed

Verdict: Case A. No page in the corpus is falsified by this diff. No commit, no push, no docs edit. The branch head stays cd9f93413eb577817fdfa9d0ad8cb7867dcd079b.

Tree audited

The bot computed on 3ac8bd89f96104a667be73cc7c084bcfb60c8b90 (merge of head cd9f93413e into base 001a83b048). I did not need a detached checkout, because the docs tree is provably the same object:

git rev-parse cd9f93413e:content/docs  -> 6403078d610abb3220bcebe671482c8f65ecb780
git rev-parse 3ac8bd89f9:content/docs  -> 6403078d610abb3220bcebe671482c8f65ecb780
git diff --stat cd9f93413e 3ac8bd89f9 -- content/docs  -> (empty)

Auditing in the claude/issue-13523-history-door-schema-rebind worktree is auditing the bot's tree for content/docs. Corpus: 403 .mdx files under content/docs (438 files total), of which 190 are hand-written per scripts/docs-audit/handwritten-docs.json.

Why the bot could not answer it

Two distinct reasons, one per half of its list:

  1. The getHistory row is an identifier collision. The bot matches bare identifiers. getHistory names two unrelated methods on two different surfaces, and the one the docs page documents is not the one this diff touches. Detail in row 1 below.

  2. The six /environments/:environmentId rows matched a path literal that entered the diff as English prose, not as code. The anchor appears on exactly one added line in the whole diff, and that line is a JSDoc sentence:

    packages/client/src/index.ts +73:
    +     * `registerForBase` replay against `/environments/:environmentId` — so
    

    That is a comment explaining how the scoped exit reaches the same handler. The diff neither creates, moves, nor re-shapes any environment-scoped route. Any page naming that path was always going to match, and matching says nothing.

Searched

Corpus: content/docs/**/*.mdx on tree 6403078d61, all 403 files, grep -rn (ripgrep-equivalent literal + ERE).

Positive control (live, run before the negatives): getHistory over the whole corpus →

content/docs/kernel/contracts/metadata-service.mdx:78

One hit, on the exact page the bot flagged. The instrument reaches the corpus and would have reported the term had it been elsewhere — so the zeroes below are the corpus's zeroes, not the instrument's. Second control: ObjectStack → 181 files. Third: the page's own title string read back out of the file it lives in.

Terms swept (each run corpus-wide, releases/ and generated references/ separated out where noted):

# Term Hand-written hits
T1 previousName 0 hand-written (3 total, all generated references/)
T2 parentHash 1 (concepts/metadata-lifecycle.mdx:80)
T3 event_seq / sinceSeq 1 (concepts/metadata-lifecycle.mdx:216)
T4 MetadataEvent 2 (concepts/metadata-lifecycle.mdx:80,81)
T5 HistoryMetaItem 0 hand-written (all in generated references/api/protocol.mdx)
T6 "actor": / actor: in a sample 0 for this door
T7 bare word actor 0 for this door (all hits are auth / approvals / audit-service / AI ToolExecutionContext / record-view-auditing / webhooks)
T8 /history route path 2 (plugins/adding-a-metadata-type.mdx:226,253)
T9 change.?log all hits are CHANGELOG.md release process, or ADR-0008 architecture prose
T10 ADR-0008 verb union create | update | delete | rename | publish | revert 1 (concepts/metadata-lifecycle.mdx:80)
T11 .getHistory( call sample / .events read 0
T12 sample body with "events" 0 for this door (hits are hook events: arrays and realtime subscription lists)
T13 system-initiated / boot sync / scheduled job null-actor prose 0 hand-written

The emitter-side hand read the bot said it structurally cannot do

The bot's own limitation — "a page that states a rule by its inputs shares no identifier with the emitter that implements the rule" — is real, and the sweep did surface a hand-written page outside its 9:

content/docs/concepts/metadata-lifecycle.mdx:80 (hand-written; not on the bot's list):

- Emits a `MetadataEvent { op, ref, hash, parentHash, actor, seq, ts, source }`, where `op` is one of `create | update | delete | rename | publish | revert`.

Not falsified — it is corroborated. Three reasons: (a) the verb union it spells out matches HistoryMetaItemResponseSchema's new closed enum exactly, member for member — this page is the emitter-side statement of the very narrowing this diff transcribes; (b) it names actor but never types it, so it makes no string-vs-string | null claim to falsify; (c) it describes the internal MetadataEvent emitted by MetadataRepository.put(), not getHistory's return value, and this diff does not touch MetadataEvent. The three members it omits (version, previousName, message) are all optional in the schema, so the brace-list is abbreviated, not wrong.

No edit made: nothing here is this diff's own consequence, and touching it would be widening.

The 9 rows

1. content/docs/kernel/contracts/metadata-service.mdx — via getHistory (the row flagged most-likely). SURVIVES.

Sentence read (line 78, inside the export interface IMetadataService block):

getHistory?(type: string, name: string, options?: MetadataHistoryQueryOptions): Promise<MetadataHistoryQueryResult>;

This is not the method this diff touches. It is the kernel service contract, and the page says so in its own Callout: Source: packages/spec/src/contracts/metadata-service.ts. I byte-compared the doc line against that source:

DOC content/docs/kernel/contracts/metadata-service.mdx:78
SRC packages/spec/src/contracts/metadata-service.ts:802
==> BYTE-IDENTICAL

and git diff --name-only 001a83b048 cd9f93413e -- packages/spec/ returns 0 files. The transcription is exact against a file this PR does not touch.

Decisively, the two getHistorys do not even share a return shape:

Surface Returns
IMetadataService.getHistory (this page) MetadataHistoryQueryResult = { records, total, hasMore }
ObjectStackClient.meta.getHistory (this diff) HistoryMetaItemResponse = { events: [...] }

Different names, different members, different files, different layers. The page names no field of the history door's body — a corpus-wide scan of it for history|actor|previousName|MetadataEvent|change.?log|event_seq|seq|parentHash returns exactly two lines, 72 (a section comment) and 78 (the signature above). Its closing REST-endpoints table lists /meta/:type/:name/publish and /meta/:type/:name/rollback and has no /history row at all.

2. content/docs/plugins/adding-a-metadata-type.mdx — via the /meta/:type/:name/history path literal. SURVIVES.

Sentence read (line 253, under ## Related):

- [`GET /api/v1/meta`](/docs/api) — lists every registered metadata type; per-type items at `GET /api/v1/meta/:type`, history at `GET /api/v1/meta/:type/:name/history`

A cross-link naming the route's path only. No body, no field, no actor, no op, no code sample. The page's two other history mentions (line 8 "version history for every type", line 226 "handles list/edit/create/history") describe what the Studio Metadata Admin renders, not what the door answers. A response-shape rebind cannot falsify a path literal.

3–8. The six /environments/:environmentId rows. ALL SURVIVE — each is about environment routing generally, as suspected. Sentence read for each:

  • content/docs/api/environment-routing.mdx (line 5): "Environment-scoped routing makes the target runtime explicit in the URL:" followed by a fenced block of four path templates (/data/:object, /meta, /automation/..., /ai/...). A page about the routing mount, listing families not bodies; /history never appears, and its response-prose sweep returns 0.
  • content/docs/concepts/north-star.mdx (line 26): "Runtime identity is environment identity. Use environment, not project, for deployment/runtime scoping: OS_ENVIRONMENT_ID, X-Environment-Id, /api/v1/environments/:environmentId, and environment_id." A naming/vocabulary principle; the path is one of four spellings in a list. Response-prose sweep: 0.
  • content/docs/deployment/publish-and-preview.mdx (line 206, a table row): "Cloud environment preview | Install to a preview environment, then route clients to it via /api/v1/environments/:environmentId/... or X-Environment-Id." A deployment recipe for aiming a client at a preview env. Response-prose sweep: 0.
  • content/docs/deployment/single-project-mode.mdx (line 31, a comparison-table row): "Routing | Direct /api/v1/... routes | Optional /api/v1/environments/:environmentId/..., hostname, header, or session". Single-env vs cloud-aware host contrast. Response-prose sweep: 0.
  • content/docs/protocol/kernel/http-protocol.mdx (lines 90–92): "scoping is added by the REST server, so clients can detect dual-mode routing; environmentId is present only on the environment-scoped mount (/api/v1/environments/:environmentId/...)." About the discovery document's scoping field. Response-prose sweep across all 1355 lines: 0.
  • content/docs/protocol/kernel/metadata-service.mdx — this one does not even contain the anchor. grep -n "environments/:environmentId" returns nothing; the closest is line 204, a different path family with a different param name: "the control plane's public artifact route (/pub/v1/environments/:id/artifact[?commit=<id>]) serves exactly such URLs". That is /pub/v1 + :id, matched only through the bot's param normalization. The page contains zero occurrences of history, seq, op, ref, rollback, audit or diff — it is about MetadataPlugin bootstrap and artifactSource.

9. content/docs/ui/forms.mdx — via /environments/:environmentId. SURVIVES.

Sentence read (lines 170–172, under ## 4. The REST contract):

"Both routes are mounted under the active environment's API base. For a standalone environment that is /api/v1; for scoped deployments it is /api/v1/environments/:environmentId."

"Both routes" are GET /api/v1/forms/:slug and its sibling — the forms door, not the metadata history door. The path is named to explain the mount prefix. Response-prose sweep across all 494 lines: 0.

Release-owned page audit (read-only — not edited, and not proposed for edit)

content/docs/releases/implementation-status.mdx — audited, not falsified, nothing to file.

The anchor that put it on the list is line 190:

"Every route carries the /api/v1 prefix. When project scoping is enabled each row is additionally mirrored under /api/v1/environments/{environmentId}. The full audited route list lives in packages/rest/src/rest-route-ledger.ts."

Two checks, both pass. First, the sentence's pointer to rest-route-ledger.ts is still accurate: this diff populates a responseSchema field on one pre-existing row and adds a comment; it adds, removes and renames no route, so "the full audited route list lives in" that file remains true and the page's route inventory is unaffected. Second, the page has zero hits for getHistory, /history, actor, HistoryMetaItem, previousName or change.?log across all 545 lines — it never describes this door's response at all. No card needed, no docs-only PR needed.

One negative worth recording

The generated (not hand-written) content/docs/references/api/protocol.mdx already renders the rebind correctly at lines 1808–1830, because it is generated from the zod .describe() strings:

| **actor** | `string | null` | ✅ | Who wrote this. `null` = system-initiated (boot sync, migration, scheduled job) — never a sentinel string… |

That is the widened actor the consumer-visible delta is about, already on the published reference. It corroborates the diff rather than contradicting it, and it is out of scope for a hand-written-docs audit either way. Consistent with the at-tier finding, nothing anywhere in the corpus implies previousName is populated on this door — the only hand-written page that lists event members omits it entirely.

Gates

None run, and none owed: Case A means no file changed, so there is no tree to gate and no empty commit was made. Head is unchanged at cd9f93413eb577817fdfa9d0ad8cb7867dcd079b; docs tree 6403078d610abb3220bcebe671482c8f65ecb780.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review September 7, 2026 22:31
@os-sales
os-sales enabled auto-merge September 7, 2026 22:31
@os-sales
os-sales added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit 61b4eb3 Sep 7, 2026
49 of 54 checks passed
@os-sales
os-sales deleted the claude/issue-13523-history-door-schema-rebind branch September 7, 2026 22:58
hotlong pushed a commit that referenced this pull request Sep 8, 2026
…ot a form

The edit-side sentence predicted one shape (session-URL survives, the platform
appends a bare block) that was measured on a raw REST PATCH and is false on the
MCP wrapper: an edit sent with no footer reads back with none (n=4). The
create-side sentence is folded into the same clause because the readings on
create disagree by channel too, so a rule that names only the edit action would
still invite the generalisation that produced a duplicated footer on #16694.

The sentence now states the two axes the ledger states — CHANNEL and ACTION —
keeps the imperative half, and points at
.claude/skills/pm-dispatch/references/platform-readings.md, which is where the
per-cell readings belong. The os-dev.md twin is corrected in the same register,
in place. Both files keep their line counts (AGENTS.md 1067/1068, os-dev.md
403/403); no reading is restated in either file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P58euzUXCVJNwmhuPC9DXY
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…ot a form (objectstack-ai#16771)

The edit-side sentence predicted one shape (session-URL survives, the platform
appends a bare block) that was measured on a raw REST PATCH and is false on the
MCP wrapper: an edit sent with no footer reads back with none (n=4). The
create-side sentence is folded into the same clause because the readings on
create disagree by channel too, so a rule that names only the edit action would
still invite the generalisation that produced a duplicated footer on objectstack-ai#16694.

The sentence now states the two axes the ledger states — CHANNEL and ACTION —
keeps the imperative half, and points at
.claude/skills/pm-dispatch/references/platform-readings.md, which is where the
per-cell readings belong. The os-dev.md twin is corrected in the same register,
in place. Both files keep their line counts (AGENTS.md 1067/1068, os-dev.md
403/403); no reading is restated in either file.


Claude-Session: https://claude.ai/code/session_01P58euzUXCVJNwmhuPC9DXY

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…nd a row says whether it came from prose (objectstack-ai#18515)

Part of objectstack-ai#16696.

The docs-drift bot's contract is that a row is *reportable*: "Each row
says which anchor put it there, **so a wrong row is reportable rather
than merely annoying**." objectstack-ai#16696 read all nine hand-written rows on PR
objectstack-ai#16694 by hand and reported three shapes. Triage (comment `5578416342`)
ruled a **different disposition for each**, and this PR keeps them
apart.

---

## Finding 1 — the diagnosis first: this is MATCH WIDENING, not
mis-attribution

Triage's hard precondition: 「⛔ 修之前先定位是"匹配放宽"还是"归因记错"——这两者的修法相反」.

**It is the match widening.** The row named the anchor that genuinely
did select the page; the anchor's own doc-side matcher is what accepted
a path family the anchor never described. The evidence that decides it,
run through the tool's own `routePatternFor` against the real page:

```
ANCHOR TAIL : /environments/:environmentId
PATTERN     : \/environments\/(?::[A-Za-z_$][\w$]*|\{[A-Za-z_$][\w$]*\}|[A-Za-z0-9_%-]+)
              …then a NEGATIVE LOOKAHEAD on the class [\w-]. Spelled out, not written:
              this body's sanitizer eats the bang out of a lookahead opener — measured
              on the first write of this very PR, where the line came back one byte short.
PAGE        : content/docs/protocol/kernel/metadata-service.mdx
regex .test : true
  MATCH :204  matched="/environments/:id"       line="artifact route (`/pub/v1/environments/:id/artifact[?commit=ID]`) serves"
  MATCH :213  matched="/environments/env_42"    line="path: 'https://cloud.example.com/pub/v1/environments/env_42/artifact?commit=cmt_1a2b',"
```

Reading taken 2026-09-16T17:13Z, tree `8cf527f8e`. Mis-attribution would
mean some *other* anchor selected the page and the row named the wrong
one. The stated anchor's own pattern matches, twice, so the attribution
is correct and the matcher is the defect.

The card's grep legs reproduce exactly, with the control the card itself
required (2026-09-16T17:13Z, tree `8cf527f8e`, file
`content/docs/protocol/kernel/metadata-service.mdx`):

| leg | value |
|:--|:--|
| POSITIVE CONTROL `metadata` | **19** — the instrument fires |
| SUBJECT `environments/:environmentId` | **0** — a real zero |
| NEAR MISS `environments/:id` | **1**, at `:204` |
| DARK CONTROL | **0** |

### The fix, and why it is two arms

The two hits fail for two different reasons, and the card names both —
"a different path family **and** a different parameter name".

1. **A parameter written as a parameter must name the same parameter**
(either spelling: `:environmentId` and `{environmentId}` are one
parameter, `:id` is another). Kills `:204`.
2. **A concrete example value filling the tail's LAST segment must end
the documented path.** Every earlier parameter is still bounded by the
segments to its right; the last one has nothing behind it, so a value
there degrades the pattern to a prefix test on `/environments/`. Kills
`:213`.

### ⛔ What was measured and rejected

Requiring **every** match to sit at the end of the documented path — the
way the same tail is matched against a ledger row,
`route.endsWith(tail)` — kills both bad hits and reads **−36.3%**. It
also deletes `api/environment-routing.mdx`, the most on-target page of
that run, whose every occurrence is
`/api/v1/environments/:environmentId/...` with a segment after it, plus
`publish-and-preview.mdx`, `single-project-mode.mdx` and
`http-protocol.mdx`. A route *prefix* written with the route's own
parameter named IS a page documenting that route. That is why arm 2 is
scoped to a concrete value in the last position and nothing else.

### Corpus measurement behind the narrowing

Denominator stated: the **228** distinct route tails declared in this
repo's **28** route-ledger files, against all **195** hand-written docs
(`scripts/docs-audit/handwritten-docs.json` — the same denominator the
tool's own recall figures use). Taken on tree `fef76a4aa`,
2026-09-16T17:45Z. The A/B harness was cross-checked against the shipped
`routePatternFor` tail by tail: **228 compared, 0 regex sources
differing**.

| variant | tail×page rows | delta | tails matching NO page |
|:--|--:|--:|--:|
| before | 571 | — | 119 |
| arm 1 only | 566 | −0.9% | 119 |
| **shipped (arm 1 + arm 2)** | **524** | **−8.2%** | **119** |
| rejected end-of-path variant | 364 | −36.3% | 120 |

**Zero tails go from matching some page to matching none.** Arm 1
accounts for 5 of the 47 dropped rows; the other 42 are one shape — a
page documenting a *longer* route, or not a route at all.
`/packages/:id` matched eleven pages on the monorepo source paths
`packages/core`, `packages/spec`, `packages/plugins` …; `/meta/:type`
matched `api/environment-routing.mdx` on the prose
*"data/meta/AI/automation"*.

---

## Finding 2 — the reading first, then the argument

⚠️ Neither triage nor the PM had taken this reading. It is the card's
own command, run before anything was built on it. Reading taken
2026-09-16T17:14Z against PR objectstack-ai#16694's diff `001a83b048...cd9f934`:

```
$ git diff 001a83b...cd9f934 -- packages/client/src/index.ts | grep -n 'environments/:environmentId'
73:+     * `registerForBase` replay against `/environments/:environmentId` — so
```

**Exactly one line, added, and it is English prose inside a JSDoc
block:**

```
+    /**
+     * The durable change-log for a metadata item, scoped to this
+     * environment. Reaches the SAME handler as the unscoped twin — one
+     * `registerForBase` replay against `/environments/:environmentId` — so
+     * the body is byte-identical and the declaration must be too.
```

Controls for that reading: the same diff for that file is **102 lines**
with **41** `+`-prefixed lines, so the instrument reads a non-empty
diff; a dark control (`zzz_no_such_token_zzz`) over the same diff
returns **0**. Across the whole diff the anchor occurs **3** times —
once in `.changeset/history-door-schema-rebind.md`, once in
`packages/client/src/index.ts`, once in
`packages/client/src/return-type-precision.test.ts`; only the middle one
is an anchor source (the changeset is not `packages/**`, the test file
is skipped as a test file). ⇒ the card's claim holds.

**Count correction, stated rather than repeated:** the card and PR
objectstack-ai#16694's own audit comment both say *six* rows carried this anchor.
Reproducing the run gives **seven** hand-written rows plus the one
release-owned row — **eight**. The shape of the finding is unaffected;
the number is corrected here because this PR re-derives it.

### The option taken: MARK the row, ⛔ do not exclude comments

Triage: 「要么把注释排除出锚源,要么在行里标注锚来自注释……⛔ 不要两个都做成硬排除:JSDoc
里新增一条真实路由的文档,有时**正是**该页需要更新的信号」.

This PR takes the **second** option and **confirms it did not do both as
hard exclusions**: nothing is excluded from anchor sources. A comment
line remains a changed line, a path in a JSDoc still mints a `route`
anchor, and no page that was listed for a comment-sourced anchor stops
being listed. The comment mask is read **only to word the provenance
clause** — `anchors[].from` is publication, never discrimination,
exactly as the objectstack-ai#12824 ruling set it.

Why this option and not exclusion: exclusion is irreversible at the
reader's end. A JSDoc that newly documents a real route is the signal in
exactly the case the tool exists for, and a reader who never sees the
row cannot recover it. A marked row costs a glance and keeps the recall.

Rows on PR objectstack-ai#16694 before → after:

```
before: /environments/:environmentId (route, a path literal in meta)
after : /environments/:environmentId (route, a path literal in a comment in meta)
```

That distinction also turned out to be load-bearing for the *second*
route anchor on that PR: `/meta/:type/:name/history` likewise enters
only through a `//` comment — the line is `+ // [objectstack-ai#13523] The change-log
body of GET /meta/:type/:name/history, the one`. Both route anchors on
that run are comment-sourced, and now both say so.

---

## Finding 3 — left alone

`IMetadataService.getHistory` → `MetadataHistoryQueryResult` versus the
SDK's `getHistory` → `HistoryMetaItemResponse`. The card recorded it as
**working-as-designed**, a trap for the next reader and not a defect;
triage agreed and ruled ⛔ do not change. **Nothing in this PR touches
it.** Its row is byte-identical before and after:

```
content/docs/kernel/contracts/metadata-service.mdx
  via getHistory (sdk, the bare tail of client method meta.getHistory, bound to GET /api/v1/meta/:type/:name/history)
```

Triage noted the row should say "锚为裸方法名" — it already does (`the bare
tail of client method …`, objectstack-ai#12824), so there was nothing to add.

---

## Two-direction acceptance

⛔ Triage's fence: 「一个"把弱命中一律丢掉"的实现会让本卡三条全绿,同时把工具的价值删掉」, and the card's
own first ⛔: *"Not that the bot should be quieter."* Both directions are
exercised.

### Direction 1 — the wrong row is gone

`node scripts/docs-audit/affected-docs.mjs 001a83b --json`, run in a
detached worktree at PR objectstack-ai#16694's head `cd9f93413e` (whose `content/docs`
tree is the object the bot computed on). **10 rows → 9.** The single
drop:

```
DROPPED  content/docs/protocol/kernel/metadata-service.mdx
         ['/environments/:environmentId (route, a path literal in meta)']
```

Every other row is kept; seven are re-claused with the comment marker;
the `getHistory` row is untouched. The run before the change reproduces
the bot's published comment on PR objectstack-ai#16694 exactly — the same 9
hand-written rows and the same 1 release-owned row, with identical `via`
clauses.

### Direction 2 — a genuinely falsified row is still listed, with a
strong-hit anchor

PR objectstack-ai#16689 is the sibling sweep the card cites, where the hand read found
rows that **were** genuinely falsified and fixed (its own answer
comment: "2 falsified and fixed, 1 not falsified"). Same command, base
`c8e5ac645f`, head `947426a7b8`:

**7 rows → 7 rows, every `via` clause byte-identical.** The page that PR
objectstack-ai#16689 recorded as FALSIFIED and then fixed,
`content/docs/getting-started/your-first-project.mdx`, is still listed,
and its anchors carry no comment marker — a strong hit:

```
KEPT  getting-started/your-first-project.mdx
      ['os create (command, read off packages/cli/src/commands/create.ts)',
       'os init (command, read off packages/cli/src/commands/init.ts)']
```

### And in the self-test

A new battery, `ROUTE-ANCHOR PRECISION, IN BOTH DIRECTIONS (objectstack-ai#16696)`, 20
cases, every narrowing case paired with a KEEP case from the same run —
including the positive control that `/data/:object` still matches a page
writing `POST /api/v1/data/accounts`, and that a comment-only anchor is
**still an anchor** and merely gains a clause.

**Reverse verification**, from the committed state, each leg proved on
disk by blob hash and restored with `git checkout HEAD -- PATH`:

| ablation | self-test exit | what failed |
|:--|--:|:--|
| revert the whole narrowing (restore the old widened parameter arm) |
**1** | 6 assertions, all of them the narrowing cases; ⛔ every KEEP case
stayed green |
| blind the comment mask (`masked = lines`) | **1** | 4 assertions, all
of them the provenance clauses |
| restored | **0** | 605 cases pass; `git diff HEAD` empty, blob
`6010e3c0` = HEAD blob |

---

## ⛔ No occurrence rate is asserted

The card: 「One PR is not a population」. Triage: 「⛔ 修卡的人也不例外」. This PR
measures **one** PR's rows on objectstack-ai#16694 and **one** on objectstack-ai#16689, and makes no
claim about how often either shape occurs. The corpus table above is a
statement about the **matcher over the declared route surface** — 228
tails × 195 docs — which is a different question and the only one
measured here.

## Not touched

`content/docs/releases/` — untouched. The
`releases/implementation-status.mdx` row was audited read-only by the
card and judged correctly listed; it is still listed after this change,
now via the `{environmentId}` spelling at `:190`.

## Changeset — `skip-changeset`, measured

The sole criterion is whether anything **published** moves, read off
each package's actual `files[]`, ⛔ not off the path name:

- **70** non-private `package.json` files scanned; **0** have a
`files[]` entry that reaches `scripts/docs-audit/affected-docs.mjs` or
`scripts/docs-audit/README.md`.
- **Positive control** that the scan reads real entries, and the exact
shape that made this reflex wrong on card objectstack-ai#18169: **147** non-`dist`
`files[]` entries exist across those packages, and they are dominated by
`README.md` / `CHANGELOG.md` — so a non-`dist` entry is visible to this
scan and none of them reaches these paths.
- **0** published packages name a `scripts` entry in `files[]` at all.
- The root `package.json` is `private: true`.
- Symbol leg: `routePatternFor` occurs in exactly one file in the tree —
the changed one. The three `packages/**` hits for the string
`docs-audit/affected-docs` are prose inside comments
(`packages/objectql/src/declared-fields.ts:183`,
`packages/spec/scripts/build-schemas.ts:1259`) and a JSON `description`
field.

## Gates

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from this worktree — **33** commands. Every
one recorded in the report. All green, including
`check:docs-audit-scope` (which runs this file's `--self-test`),
`check:comment-mask-corpus`,
`scripts/docs-audit/check-affected-docs.mjs` and
`scripts/docs-audit/check-drift-comment.mjs`.

## Acceptance notes

Noted here rather than filed — none is a reproducible defect, a
declared-contract violation, or a metadata-authoring trap:

- The card and PR objectstack-ai#16694's audit comment both say *six* rows carried the
`/environments/:environmentId` anchor; reproducing the run gives seven
hand-written plus one release-owned. An arithmetic slip in a narrative,
corrected above, not a defect in code. Carrier: this PR body.
- `/packages/:id` matching monorepo source paths such as `packages/core`
on eleven pages is the same widening shape as finding 1, and this change
removes it as a side effect. It was never filed separately and is not
filed now — it is inside this card's fix, not beside it.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_


---
_Generated by [Claude Code](https://claude.ai/code)_

---------

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

Projects

None yet

2 participants