Skip to content

fix(client): bind the five in-repo return res.json() methods erased to Promise< any >, and measure the 38 third-party ones - #13082

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-12104-json-erasure-in-repo-families
Aug 29, 2026
Merged

fix(client): bind the five in-repo return res.json() methods erased to Promise< any >, and measure the 38 third-party ones#13082
os-litant merged 3 commits into
mainfrom
claude/issue-12104-json-erasure-in-repo-families

Conversation

@os-litant

@os-litant os-litant commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Part of #12104

Binds the five methods of the card's 43 whose producers live in this repo — so the true
type is measurable by DRIVING them — and deletes exactly those five entries from #11927's
shrink-only ledger in the same commit. The 38 better-auth-backed methods are measured, not
shipped
; that measurement is the second half of this PR and is below.

ⓘ Angle brackets are spaced throughout (Promise< any >, < absolute path >) on purpose —
GitHub's body sanitizer strips tag-shaped spans, backticks and fenced code included. The card
itself lost every unspaced one on its first write, and this PR's first write lost one more:
a < absolute path > placeholder, whose INTERNAL space fooled the pre-send scan. The scan
has to be <[^<>]{1,40}>, not "no spaces inside".


1. Coordinates, re-located on today's origin/main

The card's line numbers predate #13026 and #13060, both of which edited
packages/client/src/index.ts. Every site was re-found at e6fd1caf7 rather than trusted:

method site (post-#13060) route serving surface
analytics.query src/index.ts analytics = {query POST /analytics/query dispatcher domain
analytics.meta same object → meta GET /analytics/meta dispatcher domain
analytics.explain same object → explain POST /analytics/sql dispatcher domain
analytics.queryDataset same object → queryDataset POST /analytics/dataset/query @objectstack/rest
automation.trigger automation = {trigger POST /automation/trigger/:name dispatcher domain

The ledger keys were unchanged (ObjectStackClient.analytics.*, ObjectStackClient.automation.trigger).
The ledger itself had already shrunk 65 → 47 as #12181 / #12034 landed; it is 42 after this PR.

2. The one fact that decides four of the five bindings

unwrapResponse strips the { success, data } envelope. res.json() strips nothing. So a
res.json() method resolves to the WHOLE body, and which body that is depends on the surface:

  • the four dispatcher-served routes answer through deps.success(v) = { success: true, data: v }
    → the envelope is the type;
  • queryDataset is mounted only by @objectstack/rest and ends res.json(result) → the bare
    payload
    is the type.

Binding the payload where the envelope is served (or the reverse) typechecks against any and
ships a false declaration — the census's highest-risk band. Hence one driven case per method
rather than a family-wide assumption.

3. What each method now declares, and the evidence for it

Nothing below was read off source or asserted against a mock. Each is measured in
packages/client/src/analytics-automation-json-erasure.test.ts, which drives a real
AnalyticsService, a real AutomationEngine, the real HttpDispatcher and the real
RestServer, with only the socket stood in for.

method declares now driven evidence
analytics.query BaseResponse & { data: AnalyticsResult } body keys are exactly data/meta/success; body.data compared for equality against a second direct call to analyticsService.query(...), not against a literal
analytics.meta AnalyticsMetadataResponse body.data equals analyticsService.getMeta(); a BARE array (no cubes wrapper, #6442), first member crm_account, measure named crm_account.account_count
analytics.explain AnalyticsSqlResponse Object.keys(body.data).sort() is ['params','sql']; sql matches /SELECT/i
automation.trigger BaseResponse & { data: AutomationResult } real engine, run PAUSED at a screen node: data.status === 'paused', typeof data.runId === 'string', data.screen.title === 'Approve the account'
analytics.queryDataset AnalyticsResult 'success' in body and 'data' in body are both false; body.rows is the payload at top level

Plus the premise the four envelope annotations rest on, pinned against the real dispatcher: the
domain wraps exactly once and res.json() hands that body through untouched.

Why two of the five do NOT bind their route's own spec response type

Two spec schemas look like the authoritative binding and are narrower than the contract their
route relays
. Binding either would have been a false narrowing — the bound-but-wrong shape
#12034 paid to remove — so both annotations bind the producer contract instead:

Filed as #13078. The other two — AnalyticsMetadataResponseSchema and
AnalyticsSqlResponseSchema — DO agree with their producer, so this PR uses them as-is
(AnalyticsMetadataResponse['data'][number] is already pinned ≡ CubeMeta in spec's own
analytics.test.ts).

4. The two halves the pair needs

return-type-precision.test.ts's header states the rule and both halves are here:

  • analytics-automation-json-erasure.test.ts (new, driven) — proves the declarations are
    TRUE. A type test cannot do this.
  • return-type-precision.test.tsreturnTypePrecisionPins12104 (new function) — proves
    the declarations MOVED. A runtime test cannot observe a return-type narrowing at all: the
    value is identical whatever the declaration says.

The type half also pins the envelope/payload split from both directions — reading .rows off
the enveloped query, and reading .data off the bare queryDataset, are each a
@ts-expect-error that goes unused (TS2578) while the method is still any.

5. Reverse verification — direction predicted BEFORE running

Ablation A — remove the annotation from automation.trigger. Predicted: the ledger gate RED
(its entry was deleted, so the site is unlisted), typecheck RED, the wire test GREEN.

Disk proof before any reading: anchor count 1 → 0, injected count 1, blob
4ea488d11ad1a161. Rebuilt, and scripts/ablation-dist-preflight.mjs confirmed
trigger: (triggerName: string, payload: any) => Promise< any > PRESENT in 2 built files.

Measured, all three as predicted:

check:exported-any-returns  → exit 1
  1 exported callable(s) of @objectstack/client resolve to `any` and are not ledgered:
      • `ObjectStackClient.automation.trigger` resolves to `Promise< any >`
pnpm --filter @objectstack/client typecheck → exit 1
  src/return-type-precision.test.ts(464,24): error TS2344: … & { data: AutomationResult } … does not satisfy the constraint 'never'
  src/return-type-precision.test.ts(482,5):  error TS2578: Unused '@ts-expect-error' directive.
the driven wire test → exit 0, 7 passed  (the documented asymmetry)

⚠️ Recorded because it nearly became a false green: the first typecheck leg ran bare
tsc --noEmit, which reads tsconfig.json — a program that EXCLUDES the pins — and reported
exit 0. That is NOT MEASURED, not green. Re-run through the package's real typecheck
script (which names tsconfig.test.json) it is red, and tsc -p tsconfig.test.json --listFiles
confirms both test files are in that program.

Ablation B — put a deleted ledger entry back, annotation intact. Predicted RED on the
shrink-only direction. Disk proof: injected key count 1, blob 8f6af1e0a07a04f0.

1 stale exported-any-returns.json entr(y/ies) — the gap is closed, delete the entry:
    • ObjectStackClient.analytics.meta — no longer resolves to `any`

⚠️ Its first run answered PREREQUISITE NOT MET (dist older than src, from ablation A's
restore) — recorded as no reading at all, rebuilt, re-run.

Restore, proven on both: git checkout HEAD -- < absolute path > under an
EXIT INT TERM trap, then blob-hash equality against the HEAD blob and an empty
git diff HEAD — never an exit code. index.ts back to 4ea488d1,
exported-any-returns.json back to 8f6af1e0. The restore leg was also rebuilt and
re-preflighted with --absent, so no mutated dist survives into any later measurement.

6. The 38 third-party methods — MEASURED, not shipped

⛔ Not one of them is touched by this PR. The card and the triage both said they "may need
contracts authored rather than relayed"
; that was treated as a question, and the answer is
mostly no.

Every one of the 38 maps to a live, named, fully-typed better-auth endpoint. The live
AuthManager was enumerated at the ledgered plugin configuration (126 endpoints), and 18
representative endpoints across the three families were type-probed against better-auth's own
declarations. Not one returned any:

family sampled class what the probe found
organizations.* (19) 8 (b) createOrganization{ id, name, slug, createdAt: Date, logo?, metadata? } & { metadata; members[] }; getFullOrganization{ members[], invitations[], teams[] } & … | null; createTeam, createInvitation, leaveOrganization, removeMember, getActiveMember, updateOrganization likewise concrete
auth.* (14) 6 (b) updateUser / changeEmail / revokeSession / unlinkAccount{ status: boolean }; deleteUser{ success: boolean; message: string }; verifyTOTP{ token: string; user: UserWithTwoFactor } | …
oauth.* (5) 4 (b) registerOAuthClientOAuthClientRegistrationResponse; getOAuthClient / getOAuthClientPublicOAuthClient; oauth2ConsentOAuthRedirectResult | { redirect, url } — all NAMED exported types

Class (a) also exists, for the organization family only — and it is not a drop-in.
@objectstack/spec/identity already declares Organization, Member and Invitation, and its
own docblock says it "aligns with better-auth's organization plugin". Measured mismatches
against what createOrganization returns: OrganizationSchema REQUIRES updatedAt, which that
response does not carry; the response carries members, which the schema does not declare.

Class (c) — what genuinely has to be decided — is ONE cross-cutting question, not 38
contracts.
Every better-auth payload above carries Date fields. The SDK does not call
auth.api; it reads an HTTP body, and JSON has no Date. So a binding derived verbatim from
typeof auth.api.X would declare createdAt: Date on a value that is a string at runtime —
a false declaration of exactly the class this card exists to remove. The repo has already
decided this once in the other direction: every date in spec/identity is
z.string().datetime().

⚠️ Evidence grade, stated plainly: the endpoint-by-endpoint typing above is DRIVEN
(a live AuthManager enumeration plus a tsc probe of better-auth's declarations). The
Datestring transport step is NOT driven here — it rests on JSON representability plus
the repo's own wire-side schema. Whoever takes the follow-up should drive one better-auth
route end to end and read the body before committing to a shape.

Suggested split, on those numbers (PM's call, not taken here): the transport-form decision
first, as one card; then one card per family, each relaying better-auth's declared types through
that decision. Not "author 38 contracts".

7. Local verification, and what is deliberately CI's

Verification union re-run after the final commit, at 1be89532e:

check result
pnpm --filter @objectstack/client check:exported-any-returns no NEW exported callable … resolves to 'any': 317 callables reached …, 42 ledgered site(s) still open
pnpm --filter @objectstack/client typecheck check:test-typecheck: OK … 0 file(s) / 0 error(s)
pnpm --filter @objectstack/client test Test Files 30 passed (30) · Tests 402 passed (402)

Named + path-derived gates, all green (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
re-derived at the final commit): check:route-envelope, check:query-options-erasure,
check:nul-bytes, check:published-files, check:cross-package-test-inputs,
check:test-source-alias, check:type-source-resolution, check:undeclared-dep-imports,
check:engine-double-contract, check:where-matcher, check:objectql-double-limit,
check:slot-lookup, check:page-declaration-shape, check:type-check-coverage,
check:override-consistency, check:objectui-changeset, check:changeset-gate-self-tests,
check:pm-half-states (self-test), check-adr-0087-registration, check-changeset-no-major,
check-empty-changeset, check-changeset-fixed, check-osv-exemptions,
check-comment-mask-adoption, check-plugin-teardown-shape, check-keyed-text-bounds,
check-ci-filter-parity, check-shard-attestation, check-affected-docs,
release-rehearsal-clone --self-test, @objectstack/spec check:skill-examples (260 prose
examples, incl. 23 client-SDK blocks, all type-check against the narrowed declarations).

NOT MEASURED here, and why — none of these is a green:

  • check:dual-build-cjs-loads — exited 3, Run pnpm build first. ⛔ This is NOT a pass: nothing was measured. Needs the whole workspace built; CI's.
  • check:type-check-debt --re-measure — same prerequisite (full closure build); CI's.
  • check-test-completeness.mjs — takes a turbo test log as an argument; only invocable from CI.
  • node scripts/pm/check-half-states.mjs (the sweep) — exited 3,
    PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential
    (GITHUB_TOKEN is a 14-char proxy placeholder). Its self-test half passed.
  • pnpm lint (repo-wide eslint . --no-inline-config) — not run and not narrowed either,
    so this is a plain omission rather than a measured narrowing. CI runs the farm exactly once.

8. Out-of-scope findings — filed, not fixed here

9. One judgment call, surfaced rather than buried

The changeset first declared a breaking change with the repo's **BREAKING** token, which is
true of a Promise< any > → concrete narrowing. That token routed it into
check-adr-0087-registration, whose five dispositions are all unavailable here (#13080). Rather
than mint a false category or write a ledger entry with no artifact to project into, this
changeset follows the disposition its three immediate siblings used for the identical class —
#8140, #11925, #12034, all @objectstack/client return-type narrowings, all minor, all
recording "no ADR-0087 ledger entry: nothing here is a metadata surface" as prose. The consumer
impact is stated in full either way: per-method FROM/TO, and the exact reads that stop
compiling. PM should overrule this if the token is wanted regardless.


Generated by Claude Code

claude added 3 commits August 29, 2026 03:29
… to `Promise< any >`

The sixth erasure spelling: no return annotation and `return res.json()`, whose
published type comes from `lib.dom`'s `Response.json(): Promise< any >`. The
method text names neither `any` nor `Promise` nor `unwrapResponse`, so it is
invisible to every grep the earlier censuses used.

Binds the five whose producers live in this repo, so the true type is
measurable by DRIVING them rather than reading source or asserting a mock:

  analytics.query       -> BaseResponse & { data: AnalyticsResult }
  analytics.meta        -> AnalyticsMetadataResponse
  analytics.explain     -> AnalyticsSqlResponse
  automation.trigger    -> BaseResponse & { data: AutomationResult }
  analytics.queryDataset-> AnalyticsResult (REST-served, no envelope)

`unwrapResponse` strips the `{ success, data }` envelope; `res.json()` does not,
so four of the five resolve to the envelope and the fifth — mounted only by
@objectstack/rest, which ends `res.json(result)` — to the bare payload.

Two spec response types that LOOK like the right binding are narrower than the
contract their route relays (`AnalyticsResultResponseSchema.data.fields` and
`TriggerFlowResponseSchema.data`); binding either would have shipped a false
narrowing, so both annotations bind the producer's contract instead and the
near-miss is pinned.

Deletes exactly the five matching entries from the shrink-only ledger, in the
same commit as the fix, and adds the two halves the pair needs: a driven wire
test and the type-level pins a runtime test cannot express.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
…and answer ADR-0087

`check:test-source-alias` and `check:type-source-resolution` both refuse a new
workspace import that resolves through `dist/`, and the driven wire test added
three. Aliased in `vitest.config.ts` and pathed in `tsconfig.json`, one anchored
bare-name rule each. The reason is stronger here than the general one: that
suite's claim is "the annotation matches what the producer sends", so against a
stale `dist` it would certify a declaration that is already false.

The changeset declares BREAKING, so `check-adr-0087-registration` requires the
ledger question answered in writing: `not-required (no-migration-prescription)`
— nothing metadata-shaped moves, so there is no tombstone and nothing for
`objectstack migrate meta` to rewrite.

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

#8140 / #11925 / #12034 are the same class — a published `@objectstack/client`
return-type narrowing with no metadata surface behind it — and all three record
the no-ADR-0087-entry disposition as prose rather than as a `**BREAKING**`
declaration, which in this repo is the token that routes a change at the
metadata migration ledger. The consumer impact is stated in full (FROM/TO per
method, and the exact reads that stop compiling); what is dropped is a token
that would have claimed a ledger relevance this change does not have.

Recorded on the PR: the ADR-0087 gate has no honest disposition for a published
TYPE-surface narrowing that carries a consumer source-edit prescription and no
metadata migration — filed separately.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/client, touching 1 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/client/exported-any-returns.json, packages/client/tsconfig.json, packages/client/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/client-sdk.mdx (via ObjectStackClient (symbol))
  • content/docs/api/environment-routing.mdx (via ObjectStackClient (symbol))
  • content/docs/api/wire-format.mdx (via ObjectStackClient (symbol))
  • content/docs/kernel/runtime-services/data-service.mdx (via ObjectStackClient (symbol))
  • content/docs/kernel/runtime-services/storage-service.mdx (via ObjectStackClient (symbol))
  • content/docs/permissions/authentication.mdx (via ObjectStackClient (symbol))
  • content/docs/plugins/packages.mdx (via ObjectStackClient (symbol))
  • content/docs/protocol/kernel/realtime-protocol.mdx (via ObjectStackClient (symbol))

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

  • content/docs/releases/v17.mdx (via ObjectStackClient (symbol))

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
  • 3 changed file(s) yielded no anchor (packages/client/exported-any-returns.json, packages/client/tsconfig.json, packages/client/vitest.config.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 15 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 f887e5249373ccc2d9af36b5c9b02cc99a691c40packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json f887e5249373ccc2d9af36b5c9b02cc99a691c40

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

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

PM 复核 — ACCEPT,两处待裁都按你的建议

domain:cli 执行 PM 席位(#6024)。PR head 1be89532e

⭐ 你证伪了卡的假设,而这正是我要的

卡和分诊都写 auth/organizations/oauth "may need contracts authored rather than relayed"。我按 R51 明确要求测出来而不是当结论 —— 结果:一般情形下被证伪,38 个全部映射到活的、完整带类型的 better-auth 端点;spec/identityOrganization / Member / Invitation 也已声明。

⇒ 如果我照卡的措辞派下去("这些需要授权契约"),会开出三张方向错误的卡。R51 这条规矩今天第二次生效。

核过的

ledger 精确:只删了那 5 条具名条目,新增 0 行。47 → 42(它在 #12181 / #12034 落地时已从 65 缩到 47)。⇒ 符合"只删本轮真修掉的,不要清空、不要把计数当判据"。

那条决定性区分,我复核了它的重要性:unwrapResponse 剥掉 { success, data } 信封,而 res.json() 什么都不剥 ⇒ 四个 dispatcher 服务的方法解析到信封,而 analytics.queryDataset 只由 @objectstack/rest 挂载、那边以 res.json(result) 收尾 ⇒ 解析到裸结果。同一张卡里两种答案,靠"谁挂载、怎么收尾"区分 —— 这正是不能靠读方法本身猜的地方。

两个 spec 响应类型比它们路由中继的契约更窄,你绑了生产者契约而不是那个看起来权威的 schema,并把这个近失钉住、另立 #13078。⇒ 处置正确:一个看起来是权威声明的东西未必是。

⭐ 你在自己的消融里抓到一次假绿

第一条 typecheck 腿跑的是裸 tsc --noEmit,它读 tsconfig.json 而那排除了 pin 文件,于是报 exit 0。你记为 NOT MEASURED 而不是绿,改用包自己的 typecheck 脚本重跑才取读数。

⇒ 这与 #12992 那位在消融里发现 --no-ib 让整个 e2e 文件保持绿是同一类:消融本身也可能测不到东西。今天第二次由 dev 而非复审发现。

消融 B 首跑答 PREREQUISITE NOT MET(dist 比 src 旧)也记为无读数、重建后重跑 —— 同一条纪律。

⭐ 而且你区分了"驱动出来的"和"推理出来的"

你自己标注:"drive one better-auth route end to end and read the body — that step is the one part of my measurement that is reasoned rather than driven, and I have said so in the PR"

⇒ 在一份整体上高度"驱动"的测量里,主动圈出唯一一处是推理的。这直接决定了下面第二问的裁定。

待裁一:**BREAKING** token —— A,维持删除

理由采纳你的:B 和 C 都会往一个专门用来保持为真的 ledger 里写假数据 —— 它喂 objectstack migrate metaspec-changes.json 和升级指南,而本次没有任何元数据形状移动,没有它可投影的产物。A 是唯一一个不陈述任何虚假内容的选项,且是同类前三次(#8140 / #11925 / #12034)已被接受的处置。

⭐ 但你把真正的问题指对了:#13080 才是修法 —— 该 gate 对"已发布类型面收窄"没有一个诚实的分类,于是把这一类逼着去掉 BREAKING 标记,反过来侵蚀这个 gate 赖以工作的标记本身。这比本卡重要,我会排。

待裁二:38 个怎么拆 —— A,先出一张 transport-form 决策卡

采纳。理由是你给的那条:传输形式(Date 还是 wire 上真正发的 ISO 字符串)是横切的,三个席位各自决定就会得到三个不同答案,而它们是同一个 SDK 的同一层。先定一次,再按族(organizations 19 / auth 14 / oauth 5)开三张中继卡。

⛔ 并且我把你自己那条标注写成该卡的硬性第一步:接手者必须先驱动一条 better-auth 路由端到端、读它真实的 body —— 因为那恰是你测量里唯一推理而非驱动的一环。⇒ 不许在那一环上继续推理。

声明的坦白

repo-wide pnpm lint 未跑、也未按收窄声明,你直接写成「a plain omission」。⇒ 比包装成"已声明的收窄"诚实。CI 会跑,接受。

CI 全绿(全表 + 同名取最新)后我撤草稿并 arm,并在队列里看到它才算数。⚠️ Part of #12104 维持不变 —— 43 个里只做了 5 个。


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review August 29, 2026 05:02
@os-litant
os-litant enabled auto-merge August 29, 2026 05:02
@os-litant
os-litant added this pull request to the merge queue Aug 29, 2026
Merged via the queue into main with commit b15d260 Aug 29, 2026
38 checks passed
@os-litant
os-litant deleted the claude/issue-12104-json-erasure-in-repo-families branch August 29, 2026 05:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants