Skip to content

feat(scripts,adr): a published TYPE-surface narrowing gets an honest ADR-0087 disposition — the sixth category, paid for by a base-side type reading - #13596

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-13080-adr0087-type-surface-only
Aug 31, 2026
Merged

feat(scripts,adr): a published TYPE-surface narrowing gets an honest ADR-0087 disposition — the sixth category, paid for by a base-side type reading#13596
os-zhuang merged 3 commits into
mainfrom
claude/issue-13080-adr0087-type-surface-only

Conversation

@os-project-manager

@os-project-manager os-project-manager commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes #13080

⚠️ GOVERNED SURFACE — this PR is deliberately parked as a draft, awaiting a human merge.
Its diff touches docs/adr/**. Under AGENTS.md Prime Directive 14 authoring is open to every
seat and landing is reserved to the maintainer. It is not armed, not marked ready, not
enqueued, and it is not neglected — the parked state is the deliverable state. Per the ruling
of 2026-08-30 the two halves are inseparable and ship as one PR.

ⓘ Angle brackets are spaced or spelled as placeholder words (PATH#SYMBOL, Promise< any >)
throughout this body. GitHub's body sanitizer strips tag-shaped spans, backticks and fenced
code included, so the literal marker syntax is written out in words rather than shown.

The dead end, re-measured before anything was built

A published TYPE-surface narrowing — a published SDK method whose declared return moves off
any onto the contract it always answered — declares **BREAKING** truthfully: any is
assignable to everything and admits every property read, so a consumer's code really can stop
compiling. check-adr-0087-registration then demands a disposition, and none is available.
Driven through the shipping scan() on the real #12104 shape (script in the report on the card):

disposition attempted outcome
registered SOME-ID REFUSED — no id is new in the diff, and writing one would put a prescription in the ledger that objectstack migrate meta, spec-changes.json and the upgrade guide cannot project
not-required (unpublished) REFUSED — @objectstack/client publishes to npm
not-required (already-registered SOME-ID) ADMITTED, and that is worse than a refusal: the gate verifies the id resolves and pre-dates the base, never that the entry covers the change. The only thing closing this door is the author's honesty, which is not a mechanism
not-required (no-migration-prescription) REFUSED — the body carries a FROM/TO rewrite table
not-required (runtime-interface-only ...) REFUSED — it inherits that same refusal (#8299)

So the exit the gate names in its own refusal text — "use a category that can be verified
(unpublished, already-registered)"
— is factually false for this class, and the move that
remained was to drop the **BREAKING** token. Four changesets took it (#8140, #11925,
#12034, #12104). Read individually each was reasonable; read together they are the finding, and
this PR records them in the ADR as a counter-example, never as precedent.

The sixth category

not-required (type-surface-only PATH#SYMBOL), admitted on four predicates, all four,
pinned by name and not by count (TYPE_SURFACE_PREDICATES):

predicate what it reads
published every bumped package really publishes — the exact inverse of unpublished, off the same workspace manifests
no-spec-diff the diff touches no packages/spec/** path
no-metadata-surface-diff the diff moves no Zod schema, no spec contracts/** entry, no object definition
narrowed-from-erased the named symbol reads any / unknown / unannotated at the MERGE BASE and concrete at HEAD

Predicate 4 is what makes this a narrowing rather than a hole

This is the only category exempt from the no-migration-prescription refusal. The exemption
is necessary — without it the gate still refuses the exact class the category was created for —
and it is also the only place in the vocabulary where something that detector refuses today
becomes claimable. Predicate 4 pays for it.

Predicates 1-3 alone were measured not to separate this gate's founding case: #6048 and the
#12104 shipping half come out identical on all three. The prescription refusal is the only guard
holding #6048 out (verifyRuntimeInterfaceOnly accepts its symbol without complaint), so
exempting a category from it while checking only 1-3 would hand the founding case a green exit.
Predicate 4 refuses it on a positive, re-runnable reading rather than a detector miss — the
#8299 method: #6048's ActorUser is a concretely typed exported interface at base that lost a
member, while this class starts from any.

The same case, after

The reproduction script above, re-run against this branch: every other disposition is refused
exactly as before, and type-surface-only PATH#SYMBOL is ADMITTED. Nothing else widened.

Machinery, and one thing the ruling did not have measured

Reuses parseSymbolRef / exportedTypeDeclaration as directed. No TS-aware diff reader
(predicate A was not adopted and is not present).

One assumption in the dispatch order came out partly falsified, and it is worth a reviewer's
eye: exportedTypeDeclaration matches export interface|type|class|enum and nothing else, and
three of the four live instances narrow a METHOD RETURN, not an exported type. b15d260d1
is queryDataset: async (payload) = becoming queryDataset: async (payload): Promise< AnalyticsResult > =,
with no exported type declaration anywhere in the diff. A category unable to read that shape
would be born unable to serve its own population — the #13080 defect one layer down. So this PR
adds memberReturnAnnotation, a sibling of exportedTypeDeclaration: it reads one named
symbol, in one file, at one rev, and looks at no diff. Comment and string spans are masked
through the house scanner (js-comment-mask.mjs), never a private stripper.

Coverage — 237 to 292 self-test assertions

  • a red case for each of the four predicates, each moving exactly one predicate off true
    so the case is attributable (TSO-R1..TSO-R4);
  • the all-four-true green (TSO-G1) — whose body carries a real framed rewrite table, so it
    is also the assertion that the prescription exemption works;
  • the exemption asserted both ways: TSO-R4 carries the identical body and must be refused
    by predicate 4, not by the prescription refusal (a negative assertion says so); TSO-R5
    shows no-migration-prescription on that same body still refuses;
  • TSO-R6 (still erased at HEAD) and TSO-R7 (a path this diff creates narrowed nothing — the
    door a decoy symbol would otherwise walk through);
  • the predicate set pinned by name (TSO-N1) plus a pin that the exported names are the
    ones verifyTypeSurfaceOnly actually evaluates (TSO-N2b) — a name that is exported and never
    evaluated is a predicate the ADR promises and the gate does not check;
  • the feat(runtime)!: 退役 ctx.user 的 roles 别名,positions 成为唯一拼法 (#6011) #6048 regression pin, in two layers, built from the REAL symbolTSO-6048b reads
    packages/runtime/src/security/actor-user.ts out of this tree at HEAD and asserts predicate 4
    reads the live ActorUser as CONCRETE (it cannot diverge from the tree, because it is the
    tree; if the file moves, that is a red saying to re-anchor, never a skip); TSO-6048c drives
    the full shipping scan() over a two-commit reconstruction of the feat(runtime)!: 退役 ctx.user 的 roles 别名,positions 成为唯一拼法 (#6011) #6048 diff claiming this
    category, and requires the refusal to name predicate 4;
  • 19 unit pins on the new readers, including the one that matters most: a Promise that is only
    part of a type must not unwrap, because a greedy match there is wrong in the admitting
    direction.

Reverse verification

Each leg mutated, proved on disk and at the referent the instrument actually reads, measured,
restored, and the restore proved by blob hash:

ablation result
the ADR half removed (committed, so git show HEAD:ADR really lacks it) RED — category documented nowhere: type-surface-only
type-surface-only removed from CATEGORIES, ADR kept RED — category documented but not accepted: type-surface-only
the prescription exemption deleted 18 self-test failures, TSO-G1 first
predicate 4's base-side refusal deleted 9 failures — TSO-6048c goes GREEN, i.e. the founding case becomes claimable. This is the measured regression the ruling priced, and the pin catches it
a predicate silently renamed TSO-N1 and TSO-N2b both fire

⚠️ Worth recording because it nearly produced a false green: the first attempt at the ADR
ablation came out exit 0. The gate reads the ADR from git at HEAD, not from the working
tree, so an on-disk mutation was invisible to it. Proving the mutation landed on disk was
necessary and not sufficient — it had to land at the referent the instrument reads.

Verification

Union re-run on the final commit e355e1eca, after the last edit:

  • node scripts/check-adr-0087-registration.mjs --self-test292 assertions over real temp git repos (real scan()/assertInputs() path)
  • the full family derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    from this diff (20 families) — all green, plus both convention-triggered obligations for editing
    a gate script (scripts/pm/bare-root-worklist.mjs --self-test, pnpm check:pm-dispatch-gates
    1017 cases pass)
  • the sibling gates whose self-tests read this script's program text:
    check-empty-changeset.mjs --self-test (118), check-changeset-no-major.mjs --self-test (116),
    js-comment-mask.mjs --self-test (35)
  • pnpm lint — repo-wide ESLint, exit 0 (not narrowed)
  • node scripts/check-nul-bytes.mjs — clean; both edited files also scanned directly for raw
    control bytes
  • node scripts/check-test-completeness.mjsNOT MEASURED, by the gate's own instruction: it
    grades a saved turbo run test log and prints PREREQUISITE NOT MET with no argument, which is
    the branch the derived family invokes. Recorded rather than reported as a failure.

Deliberately out of scope

  • The four live instances get their dispositions after this lands, per the ruling. Nothing in
    .changeset/ is touched here.
  • ⚠️ One of the four is not a member of this class: declare-search-clone-response-contracts
    bumps @objectstack/spec and adds packages/spec/src/api/protocol.zod.ts, so it fails
    predicates 2 and 3 outright. Recorded in the ADR so the backfill does not discover it by
    going red.
  • No changeset: this diff publishes nothing from any package (a CI-internal gate script plus an
    ADR), which is the repo's skip-changeset case.

Follow-up — the merge-queue ejection, fixed on fe829beaa

The queue ejected this PR on a gate in a package this diff does not touch:
packages/client/src/envelope-caller-census.test.ts went red with expected 21 to be 19.

Cause. That census walks the whole workspace for four dispatcher-served SDK spellings and
masks comments only (scripts/js-comment-mask.mjs) — never string literals. The
type-surface-only fixture's changeset body is a string literal, and its migration table
demonstrated a call on client.analytics.query: two occurrences, counted as real SDK call sites.
Measured with the repo's own maskComments, over every file this PR changes:

pattern origin/main branch before branch after
analytics.query 0 2 0
analytics.meta 0 0 0
analytics.explain 0 0 0
automation.trigger 0 0 0

docs/adr/0087-metadata-protocol-upgrade-contract.md is not scanned by that census at all — its
extension is outside the census's own extension set. The two comment mentions this PR adds are
masked correctly and contribute nothing, which is what isolates the mechanism to literals.

Fix — the fixture now demonstrates its OWN subject. TSO_REF names queryDataset and
TSO_BASE_CLIENT declares queryDataset, so a table rewriting analytics.query was prescribing
a migration this fixture's diff does not ship. The table now rewrites queryDataset, and the
direction is the honest one for it: that route is served BARE by @objectstack/rest, so there is
no envelope — .data.rows is a spelling Promise< any > used to permit and the new annotation
refuses, a prescription the COMPILER delivers, which is exactly what this category is for. The
fixture keeps a genuine FROM/TO migration table, which is what makes TSO-G1 an assertion about
the prescription exemption rather than a case that passes for another reason.

⛔ Not touched: packages/client/** (not the census, not its ledger, not its expectations), the
ADR half, and the census's coverage — nothing was masked, skipped or exempted. Registering
fixture prose as call sites would put false data in a ledger whose whole purpose is to be true.

Verification on fe829beaa — the union re-run after the last edit. The branch also carries a
clean merge of origin/main, so the generation CI now builds is the one the queue rebuilds:

  • pnpm --filter @objectstack/client exec vitest run src/envelope-caller-census.test.ts
    before: Test Files 1 failed (1) / Tests 3 failed | 10 passed (13), AssertionError: expected 21 to be 19;
    after: Test Files 1 passed (1) / Tests 13 passed (13)
  • pnpm check:adr-0087-registration292 assertions over real temp git repos (real scan()/assertInputs() path), shipping run clean
  • pnpm check:ratchet-remedy-authority176 scripts swept (scripts/*.{mjs,mts}), self-test OK. Run explicitly: it builds its population at runtime from a directory read and declares no path literal, so no path derivation names it
  • the family re-derived from this diff by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — 20 families, plus both convention-triggered obligations for editing a gate script (scripts/pm/bare-root-worklist.mjs --self-test, pnpm check:pm-dispatch-gates1048 cases pass) — all green
  • pnpm lint — repo-wide ESLint, exit 0 (not narrowed)
  • pnpm check:nul-bytes — clean; the edited file also scanned directly for raw control bytes
  • node scripts/check-test-completeness.mjsNOT MEASURED, as above: it grades a saved test log that the derived family hands it no argument for. pnpm --filter @objectstack/lint run check:doc-formula-expressions first printed PREREQUISITE NOT MET (its package was unbuilt), and is green after building it — a NOT MEASURED reading is recorded as such, never as a pass
  • every exit code captured before any pipe

No changeset, unchanged reasoning: the follow-up commit touches only
scripts/check-adr-0087-registration.mjs, the root package is private with no files array, and
no workspace package's files array carries scripts. Labels re-read live after the push:
documentation, size/l, skip-changeset — the exemption label is still on.

Filed, not fixed here#13874: the general shape, that any gate quoting example code inside
an author-facing message string is indistinguishable from a call site to this census. Bare and
unassigned; routing and priority are triage's.

Follow-up session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC


Generated by Claude Code

…ADR-0087 disposition — the sixth category, paid for by a base-side type reading

`check-adr-0087-registration` had no disposition a published TYPE-surface
narrowing could answer honestly. Measured on the real #12104 shape, driven
through the shipping `scan()`: `registered` refuses (no id is new, and writing
one puts false data in the ledger), `unpublished` refuses (`@objectstack/client`
publishes), `no-migration-prescription` refuses (the body carries a FROM/TO
table), `runtime-interface-only` inherits that same refusal — and
`already-registered` is *admitted*, which is worse: the gate verifies the id
resolves and pre-dates the base, never that the entry covers the change, so the
only thing closing that door is the author's honesty.

The exit the gate names in its own refusal text is factually false for this
class, so the move that remained was to drop the `**BREAKING**` token. Four
changesets took it (#8140, #11925, #12034, #12104). A whole recurring class of
genuinely compile-breaking change was being nudged away from this repo's
breaking-change marker, purely because the marker routed it into a ledger it
does not belong in — invisibly, with every gate green.

## The sixth category

`not-required (type-surface-only <path>#<Symbol>)`, admitted on four predicates,
all four, named rather than counted (`TYPE_SURFACE_PREDICATES`):

  published                  every bumped package really publishes — the exact
                             inverse of `unpublished`, same manifests.
  no-spec-diff               the diff touches no `packages/spec/**` path.
  no-metadata-surface-diff   the diff moves no `*.zod.ts`, no spec
                             `contracts/**` entry, no object definition.
  narrowed-from-erased       the named symbol reads `any` / `unknown` /
                             unannotated at the MERGE BASE and concrete at HEAD.

This is the only category exempt from the `no-migration-prescription` refusal.
The exemption is necessary — without it the gate still refuses the exact class
the category serves — and predicate 4 is what pays for it. Predicates 1-3 alone
were measured NOT to separate #6048 from #12104: both come out published, no
spec movement, no metadata surface. The prescription refusal is the only guard
holding #6048 out, so exempting a category from it while checking only 1-3 would
hand the founding case a green exit.

Predicate 4 refuses it on a positive, re-runnable reading rather than a detector
miss (the #8299 method): #6048's `ActorUser` is a concretely typed exported
interface at base that LOST a member; this class starts from `any`.

## Machinery

Reuses `parseSymbolRef` / `exportedTypeDeclaration`, per the ruling; no TS-aware
diff reader. One new sibling reader, `memberReturnAnnotation`, because three of
the four live instances narrow a METHOD RETURN and not an exported type — a
category that could not read that shape would be born unable to serve its own
population. It reads one named symbol in one file at one rev, and looks at no
diff. String and comment spans are masked through `js-comment-mask.mjs`.

## Coverage

+55 self-test assertions (237 -> 292): a red case for each of the four
predicates, each moving exactly ONE predicate off true; the all-four-true green;
the predicate set pinned BY NAME plus a pin that the exported names are the ones
`verifyTypeSurfaceOnly` actually evaluates; the exemption asserted both ways;
and the #6048 regression pin in two layers — a unit pin reading the REAL
`packages/runtime/src/security/actor-user.ts` out of this tree, and a scan-level
pin over a two-commit reconstruction of the #6048 diff.

The ADR half is inseparable: the vocabulary is pinned bidirectionally, and both
directions were re-measured on this branch. This PR touches a governed surface
and is deliberately left as a draft for a human merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

⏸ Standing status: green for 10 hours, and the only thing left is a human merge

domain:devx PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC, 2026-08-31 12:45Z.
⛔ No labels, state or draft flag changed by this comment. It exists because this PR has been finished
and invisible at the same time.

opened 02:45:44Z — 10 h ago
checks 29, zero pending, zero failed
mergeable_state clean
blocking ⛔ nothing mechanical

Why no AI seat has landed it. The diff touches docs/adr/0087-metadata-protocol-upgrade-contract.md
governed surface. AGENTS.md #14, on three maintainer rulings: ⛔ no AI seat merges, queues, or arms
auto-merge on a PR whose diff touches one.
This seat has not armed it and will not.

⚠️ And the honest part.This thread has had zero comments in ten hours — nothing has been
said on it since it was opened, which is itself the symptom. The repo's own governance audit records
(scripts/pm/check-governed-merges.mjs) that the draft state is not what protects a governed PR
a governed PR was merged while in draft on 2026-08-08, and the file's own conclusion is "parking a
PR as draft is not a barrier either."
⇒ The draft is not adding safety here; it is only keeping this
PR out of the maintainer's ready-to-merge view.

This seat is still not flipping it ready, and the reason is case law rather than caution:
#9319 (2026-08-17) records a .claude/skills/** PR "whose own body said 'draft, awaiting a human
merge'"
being flipped ready and enqueued by an unidentified seat, after which the queue landed it
with zero reviews. Doing that here would be a repeat, by the same shared login.

Waiting on the maintainer. The question of whether green governed PRs should be marked ready
(while remaining ⛔ unarmed) so they surface in the merge list has been put to the maintainer directly;
whatever is decided will be applied consistently and written into this seat's protocol.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 14:32
@os-zhuang
os-zhuang enabled auto-merge August 31, 2026 14:33
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ CORRECTION — this seat did NOT arm this PR. os-zhuang did.

The comment below says this seat armed #13596 into the merge queue. That is wrong, and an audit comment that misattributes who landed a governed PR is worse than none. Read from the timeline API:

reviewed             2026-08-31T14:32:38Z  by os-zhuang  (approved)
ready_for_review     2026-08-31T14:32:46Z  by os-zhuang
auto_merge_enabled   2026-08-31T14:33:01Z  by os-zhuang  (merge_method: merge)
added_to_merge_queue 2026-08-31T14:33:56Z  by os-zhuang

This seat did call update_pull_request(draft:false) and enable_pr_auto_merge(SQUASH) on this PR under the authorization quoted below — both returned HTTP 200 and both were no-ops, because os-zhuang had performed the same transitions seconds earlier. ⚠️ A 200 from those endpoints is ⛔ not evidence that this seat caused the state; the timeline actor is.

Every action that actually landed this PR was taken by os-zhuang, including the arm and the enqueue. The authorization quoted below was given to this seat and is real; ⛔ it is not evidence that os-zhuang held one, and this seat cannot determine from here whether it did.

⚠️ Flagged rather than assumed either way: an independent seat approving, un-drafting, arming and enqueueing a governed PR is the shape AGENTS.md Prime Directive 14 exists to prevent, and it is the #9319 pattern on the record. It may be entirely legitimate — the same maintainer may have instructed os-zhuang in its own session. The maintainer can tell; this seat cannot.

ⓘ The correction on the sibling PR does not apply: on #13584 the ready-for-review and the enqueue were genuinely this seat's (timeline actor os-project-manager); os-zhuang only approved it. That comment stands as written.

Original comment preserved unedited below.


⚠️ Governed-surface landing under an explicit maintainer authorization

This PR touches a governed surface — under AGENTS.md Prime Directive 14, ⛔ "no AI seat merges, queues, or arms auto-merge on a PR whose diff touches one." This seat did not arm it for 12 hours on exactly that ground.

The maintainer authorized it in this session, verbatim:

这几个都授权你合并

Given directly to the domain:devx PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC, on 2026-08-31, in the conversation where this PR and #13596/#13584 were the named subject and their governed status had just been explained in full. This is the maintainer direct-dispatch channel (2026-08-10 standing authorization): the instruction is quoted here verbatim so the authorization is auditable rather than living only in a session transcript, and it holds only for the PRs explicitly named.

What this seat did NOT do, authorization notwithstanding:

  • Not merged directly. Armed via auto-merge so it enters the merge queue, whose build runs the FULL test suite — PR-side CI runs only the affected subset. The authorization covers landing this change, ⛔ not skipping a test layer.
  • No content was changed to make it land, and no check was waived. It was already green (30 checks, zero failed) before the authorization.
  • Not generalized. This is not a precedent for other governed PRs; Prime Directive 14 stands unchanged for every PR the maintainer has not named.

If this reads as the wrong call, the merge queue is serial and this can be dequeued.


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queue Aug 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33403389633 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/client:test:  FAIL  src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > the mechanical enumeration and the hand ledger agree, site for site
      ↳ 失败原因: @objectstack/client:test: AssertionError: expected { …(13) } to deeply equal { …(12) }
    @objectstack/client:test:  FAIL  src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > ⭐ THE NUMBER: zero call sites in this repo would break SILENTLY
      ↳ 失败原因: @objectstack/client:test: AssertionError: expected [ { …(5) }, { …(5) } ] to deeply equal []
    @objectstack/client:test:  FAIL  src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > records the split: 13 loud pin sites, 6 result-insensitive, 1 not-SDK
      ↳ 失败原因: @objectstack/client:test: AssertionError: expected 21 to be 19 // Object.is equality
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/envelope-caller-census.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

⭐ Root-caused: #13596 is the real regression. #13797 and #13824 are bystanders — ⛔ do not re-queue them as "flake"

Diagnosed by the domain:devx PM seat (#6023). ⛔ No re-queue has been spent by this seat on any of the three.

The finding

envelope-caller-census.test.ts walks the whole repo from REPO_ROOT over \.(ts|tsx|js|mjs|cjs)$, matching four [namespace, method] pairs — analytics.query · analytics.meta · analytics.explain · automation.trigger. It masks comments (scripts/js-comment-mask.mjs) — ⛔ it does not mask string literals.

#13596 adds this to scripts/check-adr-0087-registration.mjs:3901, a string literal (a gate's author-facing message), not a comment:

'| `(await client.analytics.query(q)).rows` | `(await client.analytics.query(q)).data.rows` |\n';

⇒ two analytics.query( occurrences, unmasked, counted as real call sites.

Measured with the repo's own masker, ⛔ not by eye

Running maskComments from scripts/js-comment-mask.mjs over that one file at both revisions:

revision unmasked analytics.query( in check-adr-0087-registration.mjs
origin/main 0
#13596 branch 2
delta +2

⭐ And the failing assertion is expected 21 to be 19+2, exact match. The other two assertions ({…(13)} vs {…(12)}, and a 2-element array expected empty) are the same two sites surfacing in the ledger diff and the "would break SILENTLY" list.

⚠️ Note the two comment mentions this PR also adds (:2367, :2898) are correctly masked and contribute nothing — the masker is working exactly as designed. The defect is that a string literal carrying a code example is indistinguishable from a call site to this census.

What follows

⭐ This is precisely the shape #13849's own body warns about: "Start with the root above; an inherited row is a bystander until shown otherwise." The root was shown to be the root, by measurement.


Generated by Claude Code

@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ CORRECTION — the conclusion held, the MECHANISM I gave was wrong

The comment below says the census never ran on this PR's checks because "no affected-set derivation selects it." That is not why. The dev found the real reason and this seat verified it:

branch tip before the merge = e355e1eca
$ git cat-file -e e355e1eca:packages/client/src/envelope-caller-census.test.ts
  → DID NOT EXIST
$ git rev-list --count $(git merge-base e355e1eca origin/main)..origin/main
  → 98

the test file was not on this branch at all. The census landed on main after this branch was cut, 98 commits back. It was never a question of affected-set selection — there was nothing to select.

⚠️ Why the difference matters, and it is not pedantry: my explanation implied a structural gap that merging main would not close. The truth is the opposite — merging main closes it, and that is exactly what the dev did before fixing anything. The census now runs on this PR's own CI from here on, so this class of eviction becomes visible pre-queue instead of only in the queue's rebuilt generation.

⭐ The dev flagged this as "one thing the dispatch did not say, and it changes how this reproduces" — correctly. A stale branch is not merely behind; it is blind to every gate added since it was cut, and its green checks are green over a smaller world.

The operative conclusion below stands unchanged: this PR's green ticks did not cover the census, and re-arming before the fix would have ejected it again. Only the reason was wrong.

Original comment preserved unedited below.


⛔ Do NOT re-arm on the strength of this PR's green checks

State right now: draft=false, 29 checks, zero pending, zero failed, head e355e1eca — ⛔ and that is not evidence that it will pass the queue.

PR-side CI runs only the AFFECTED subset. The merge-queue build runs the FULL suite. envelope-caller-census.test.ts lives in @objectstack/client, which this PR does not touch — so no affected-set derivation selects it, and it has never run on this PR's own checks. The +2 measured here is invisible to every green tick above and will reproduce deterministically on the next queue build.

⇒ ⛔ This PR is not ready to re-queue until the fix is pushed. A re-queue now ejects it again and rebuilds everything behind it.

⭐ The general reading, worth more than this instance: "all checks green" answers a narrower question than "this will merge." A gate that is not in the affected set is not a gate that passed — it is a gate that did not run.

A fix is dispatched (card #13080, on this branch — ⛔ no new PR). This comment will be superseded by that push.


Generated by Claude Code

…ect, so its migration table stops reading as an SDK call site

The `type-surface-only` fixture's changeset body carried a real FROM/TO
migration table -- correctly, since this is the one ADR-0087 category exempt
from the prescription refusal -- but the table rewrote
`client.analytics.query`, a method the fixture neither declares nor names.
`TSO_REF` names `queryDataset` and `TSO_BASE_CLIENT` declares `queryDataset`,
so the table was prescribing a migration this fixture's diff does not ship.

That incoherence also ejected the PR from the merge queue.
`packages/client/src/envelope-caller-census.test.ts` walks the whole repo for
the four dispatcher-served SDK methods and masks COMMENTS ONLY -- never string
literals. The table is a string literal, so its two `analytics.query(`
spellings were counted as real SDK call sites and the census ledger went red
on a count it has no row for (expected 21 to be 19).

Aligning the table to the fixture's own symbol fixes both at once, and the
direction is the honest one for it: the `/analytics/dataset/query` route is
served bare by `@objectstack/rest`, so there is no envelope, `.data.rows` is a
spelling `Promise< any >` used to permit, and the new annotation refuses it --
a prescription the COMPILER delivers, which is exactly what this category is
for.

The fixture keeps a genuine migration table; nothing in the census, its
ledger, or the ADR was touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit 933ea61 Aug 31, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants