Skip to content

refactor(drivers)!: aggregate 的 query 参数收窄到 DriverQuery,并退役 aggregate / func 两个未声明别名 (#6212 批 B, #6321) - #6404

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-6212-batch-b-aggregate-signatures
Aug 7, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-6212-batch-b-aggregate-signatures

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Part of #6212 (批 B)
Closes #6321

aggregate 的 query 参数从 any 收到 DriverQuery,三处落点:SqlDriver.aggregate、TursoDriver.aggregate(转发层)、RemoteTransport.aggregate。any 在 query 参数上不是「对象名没检查」,而是检查全关——where 的 filter 方言、groupBy 的节点联合、aggregations 的节点形状,恰恰就是这三个方法体读的全部内容。

严格按派工的硬性顺序做:① 重拼 fixture → ② 删两个未声明别名 → ③ 收窄签名。倒过来会拿到一堆解释不清的红;按这个顺序走,收窄那一步在 driver-sql / driver-turso / driver-sqlite-wasm 上零报错。


① fixture 重拼(4 个文件,16 处)

aggregate: → aggregations:、func: → function:。逐处核对过语义:

  • sql-driver-advanced.test.ts 7 处、sqlite-wasm-driver-advanced.test.ts 7 处 —— 纯重拼:这些用例考的是 sum/count/avg/min/max/groupBy 的行为,别名只是拼法。field: '*' 保留不动,AggregationNodeSchema.field 是 z.string().optional(),而驱动对 '*' 有专门分支。
  • sql-driver-queryast.test.ts / sqlite-wasm-driver-queryast.test.ts 各 1 处 —— 整条替换,不是重拼。它们的标题是 should still support legacy aggregate format,钉的恰恰是本 PR 删掉的那条肢;「legacy format」在这次之后不存在了,标题说着一个已消失的东西的用例比没有用例更糟。改为 declared 拼法的同一查询(这个文件本来就是讲标准 QueryAST 键的),旧拼法的拒收另立 pin 文件。

② 退役两个未声明别名(#6321,ADR-0049)

query.aggregations || query.aggregate        // driver-sql
agg.function      || agg.func
query?.aggregations || query?.aggregate || []   // RemoteTransport
String(agg.function || agg.func || '')

QueryASTSchema 声明 aggregations,AggregationNodeSchema 声明 function;aggregate / func 在 packages/spec 里一个字都没有。这是 #4984 家族:fixture 拼着别名,宽容分支就永远绿着活下去,没有任何测试能在删掉它时转红。①做完写者归零,PD #12 + ADR-0049 于是把两条 || 删掉。

顺带删掉 || '':它只在两个键都没写时生效,那时远端面把名字回引成 ""、本地面回引成 "undefined" —— 同一份越界输入两种措辞(#5240)。别名在时这条岔路够不着,删别名恰恰让它够得着,所以同一次关掉。

driver-mongodb/src/mongodb-driver.ts:472 也读 query.aggregate,但已被 as any 罩住、且 mongodb 是批 C 的面 ⇒ 本批不碰,是已知残余,随批 C 处置。

③ 一处真实行为改动:RemoteTransport 现在会编 GroupByNode 联合

GroupByNodeSchema 是 z.union([z.string(), z.object({ field, dateGranularity?, alias? })]),这一层却按 string[] 读。收窄后 tsc 直接把这条假设摆上台面(TS2322)。处置选 (a):运行期收窄 + 残余显式拒收,无任何 as any / 类型断言。

⚠️ 派工书那句归因被实测证伪,如实记

派工书说「今天不炸的原因是 turso-driver.ts 在 remote 模式把 queryDateGranularity 声明为空表」。那只挡住了一半。 objectql engine.ts 的 aggregate 派发里逐字写着:

if (!g?.dateGranularity) return true; // plain {field} object is fine

即无 granularity 的结构化条目根本不受那个能力位约束,引擎照样下推。而 groupBy: [{ field: 'db_password' }] 这个形状是活体 —— packages/objectql/src/secret-fields.test.ts:341 就是它。于是今天:

面 groupBy: [{ field: 'region' }]
本地 / replica(SqlDriver.aggregate 读 g.field) 编成 GROUP BY "region",正常返回
remote(RemoteTransport) 插值成 "[object Object]",死在 assertSafeIdentifier,报「不安全标识符」

一条查询两种答案、由连接串决定,而且是活体不是休眠 —— 正是 #6203 那个形状。这把本批从「顺手收窄类型」抬成了「关掉一条真实分叉」,也是 (a) 之所以是对的选择的全部理由。

处置

SqlDriver 那一面的同一条件也换上了信封(PM 已裁定留在本 PR)

SqlDriver.aggregate 对「本方言编不出这个 granularity」原本抛裸 Error(code / status 皆 undefined ⇒ mapDataError 落默认分支,一个具名能力缺口以不透明 500 到达调用方)。两处触发条件逐字同一(supports.queryDateGranularity[g] !== true),只给远端加信封正是 #5907 花一整个 issue 关掉的「一条件两身份」。它们不是两个可分的改动,是一个条件的两种拼写,因此同一次落地:

- SqlDriver: dateGranularity 'week' not supported on dialect 'better-sqlite3'. Engine must fall back to in-memory bucketing.
+ Date bucketing by 'week' is not supported by this backend. Bucketed here: day, month, quarter, year (dialect 'better-sqlite3'). … (code=NOT_IMPLEMENTED, status=501)

两面首句逐字一致,尾句各报本面编得出的 granularity(这一处差异本身就是能力位的全部内容)。由一条跨包 parity 用例比对两个运行时消息钉住 —— 两边都不读共享常量,REFUSAL_SENTENCE 是第三份独立副本,所以「将来只改一面」和「两面一起改错」都会红。⚠️ 每条拒收用例都断 code 和 status,不只断「抛了」(#6144 / PR #6233:本面对这些输入原本也抛,只断 toThrow 会恒绿)。


反向验证(方向先判后跑,三组全部命中)

三组预判都写在各测试文件的 docblock 里,跑之前写的;实测数字随后补录。

A. 把两条别名分支放回去(两面),签名不动

  • pnpm test:driver-sql 2 红(expected 30 to be undefined;expected the driver to refuse the query, but it resolved)、driver-turso 3 红 / 4 绿(两条运行期 + parity)。
  • pnpm typecheck:绿,不动 —— excess-property 检查不关心方法体读什么。
  • ⚠️ 重拼后的 advanced / queryast fixture 纹丝不动:a || b 放回去仍然先读 a。所以 fixture 重拼必须在前,且不能兼作 pin —— 那四个文件此后已无法分辨别名分支在不在。

B. 把 remote 的 groupBy 读法改回 string[](只改远端面)

  • 11 红 / 2 绿(共 13),逐条命中:结构化条目 2 条在调用内抛 unsafe identifier rejected: "[object Object]";5 条 granularity 拒收红在 expected undefined to be 'NOT_IMPLEMENTED';parity 同上;'month' 非对称用例只红在远端半边;注入用例第一条断言绿、第二条红(旧代码抛的是同一句话、原因却相反 —— 所以断的是越界文本而不是那句话)。
  • 绿的两条:字符串形控制项、以及钉本地面列表的那条。
  • 无一条走「它居然编过去了」分支 —— 旧代码对这些输入本来也抛,只是消息错、无 code。即本批改的是诊断质量,不是可达性,请勿误读为新堵了一个洞。

C. 把签名改回 any

  • pnpm typecheck 红在四条 @ts-expect-error 上:TS2578: Unused '@ts-expect-error' directive.(driver-sql 116/124 行、driver-turso 106/112 行),外加 remote-transport.ts 一条 TS7006。
  • pnpm test 不受影响 —— 所以这两个 key pin 的通道是 tsc,不是 vitest,docblock 里逐条按通道写清楚了。

验证与门禁

基线:merge origin/main @ e15bf7ef7(含批 A+E PR #6355 与批 C PR #6356,两者都动过 sql-driver.ts,合并无冲突)后全仓 pnpm build 再验。

项 结果
driver-sql typecheck / test 绿 / 959 passed, 46 skipped
driver-turso typecheck / test 绿 / 828 passed
driver-sqlite-wasm typecheck / test 绿 / 254 passed
verify test(消费面) 绿 / 17 passed
spec typecheck / test(因新增台账条目) 绿 / 8644 passed
pnpm check:query-options-erasure 绿,testSurface.sites 263,无变动
pnpm check:type-check-debt(全仓 re-measure) 绿,none above its recorded number
pnpm check:nul-bytes + 手工控制字符自扫 绿
check:adr-0087-registration / check:generated 绿(见下一节)
eslint(含 query-options/no-any-erasure) 绿

⚠️ driver-sqlite-wasm 读的是 driver-sql 的 dist/*.d.ts(派工点名的门禁盲区):全程按「先 build driver-sql → 再 typecheck/test driver-sqlite-wasm」做,它在验证面内。它自身源码零改动(改的只有 fixture 与一条断言),因此按批 A+E 的先例未列入 changeset。

两条与派工书预判不同、如实记:

  1. erasure 棘轮没有下跌,本 PR 不需要 --update。 派工书预判「删多余 as any 会让 267 跌」。实测:三个驱动包里 .aggregate(… as any) 站点为 0,那 49 处全在别的包、走的是 engine.aggregate,属超范围。基线一行未动(263 是 main 上批 C 已经棘轮下来的数)。
  2. check:type-check-debt 出现过一次 §9 假红并已证伪。 新 worktree 只建了依赖闭包时,该门报 root DEBT +5 / plugin-approvals TEST_DEBT +1。把本分支改动 git stash 掉、在干净 main 上重测得到同样的 +5 / +1 ⇒ 与本改动无关;跑完全仓 pnpm build 后该门转绿。即 §9 的漏建假红也会打到这道门上([skill] 新 worktree 里第一次验证之前必须先 build 依赖闭包 —— AGENTS.md §9 的陈旧产物陷阱当日连咬三个 dev(假红 + 假绿两个方向) #6371 记)。

ADR-0087 台账处置:registered(先测后选,实测决定的)

#6148 新门(PR #6342,今天合入,仓内零先例)要求声明为 breaking 的 changeset 写明台账处置。四个标记逐个在真 commit 上实测:

标记 实测
not-required (unpublished) ❌ 硬红 —— 该格判的是 npm 包 private: true,不是「这个键从未发布」。原话:@objectstack/driver-sql, @objectstack/driver-turso are PUBLISHED
not-required (already-registered) ❌ 无既有条目覆盖
not-required (no-migration-prescription) ⚠️ 机械上能过,但只因探测器大小写敏感、只认英文大写 FROM → TO,而本 changeset 的迁移段是中文。正文确实给了改写处方 ⇒ 用它就是踩盲区过关,不写
registered + 条目 id ✅ 实测证明是诚实答案

决定性测量:在本地写候选台账条目 → gen:spec-changes + gen:upgrade-guide → 看它实际渲染成什么。判据只有一条:那行字对仓外升级者是不是真话、能不能照着做。

结果:条目落进 ### Semantic (delegated to you, with acceptance criteria),不在 ### Mechanical (applied for you) 那张带「migrate meta only」列的表里(全文命中 1 次)。渲染出来是可执行的人类指令:

- **`driver-aggregate-undeclared-key-aliases-removed`** — driver aggregate() call argument
  — query.aggregate and aggregations[].func → query.aggregations and aggregations[].function
  — the spellings QueryASTSchema and AggregationNodeSchema have always declared
  - Why not automatic: …
  - Done when: No caller passes `aggregate:` to a driver's `aggregate()` … An inline literal
    still using either old spelling no longer type-checks (TS2353 at the call site) …

⇒ 没有任何 migrate meta 暗示,没有指向不存在的入库字段。我先前判 registered 为「实质错」是错的:我把台账等同于 migrate meta,而台账的 semantic 数组恰恰是代码面的非机械残余。仓里已有三条同形先例,且它们在自己的 reason 里逐字写着这个道理:data-driver-find-stream-retired(#4484)、storage-service-list-retired(#5540)、actor-user-roles-to-positions(#6011 —— 正是这道门的立门案例)。其中 #6011 那条写着:「for an untyped or sandboxed body there is no enforced channel at all, which is exactly why this ledger entry has to exist」 —— 本例同理:收窄后 tsc 只够得着有类型的调用点,无类型 JS 调用方唯一的通知渠道就是这条台账。

于是新增一条 packages/spec/src/migrations/registry.ts 的 step-17 semantic 条目(只加这一条,PM 就此放开批 B 的「⛔ 不碰 packages/spec」范围约定),并重新生成 spec-changes.json 与 docs/protocol-upgrade-guide.md。⚠️ 条目里显式写了一句别读串的警告:dashboard/page 的 measure 那边 aggregate 是规范键、func 是已声明的响亮别名(DatasetMeasureSchema)—— 那套词表本 PR 不碰,而它很可能正是仓外调用方当初把这两个键写到 query 上的原因。

门禁:node scripts/check-adr-0087-registration.mjs → ✓ 1 declared-breaking changeset(s) … registered driver-aggregate-undeclared-key-aliases-removed (new here: …);pnpm --filter @objectstack/spec check:generated → ✓ All 10 generated artifacts are up to date(含 check:spec-changes、check:upgrade-guide);spec 自身 typecheck 绿、Tests 8644 passed (8644);改台账后三个驱动包 + verify 全部重跑仍绿(959 / 828 / 254 / 17)。

一条与本 PR 无关、但影响面更大的连带发现(已记 #6419):hasMigrationPrescription 只认英文大写 FROM → TO,而 AGENTS.md 要求解释性文字用中文 ⇒ 本仓用中文写的迁移处方一律照不到,该格的实际约束力低于设计意图。

边界

未碰:analyzeQuery / findWithWindowFunctions(批 A+E)、memory / mongodb(批 C)、distinct(#6320)、#6204 的信封与降级表、#6203 的大小写判定、#6210 的其它签名。packages/spec 只动了 ADR-0087 台账那一条新条目及其两个生成物(PM 就此放开范围约定,理由见上一节),schema / 类型 / 导出面零改动。TursoDriver.aggregate 的 options?: any 也刻意不动 —— 那是第二条轴,与它上面四个 override 逐字相同,五改其一会被读成对另外四个的判断(#6210 在 count 上同理)。

超范围发现(另立单,本 PR 不修)

…liases retire (#6212 batch B, #6321)

Three doors moved off `query: any` onto `DriverQuery`: `SqlDriver.aggregate`,
`TursoDriver.aggregate` (the forwarding layer) and `RemoteTransport.aggregate`.
`any` on a query parameter is not "the object name goes unchecked", it is every
check off on exactly the members these bodies read.

Three steps, in the order that makes the reds explicable:

1. Fixtures re-spelled to the declared keys (`aggregate:` -> `aggregations:`,
   `func:` -> `function:`) in driver-sql's and driver-sqlite-wasm's advanced +
   queryast suites. Non-test writers were zero, so this took the writer count of
   both undeclared keys to zero.
2. `query.aggregations || query.aggregate` and `agg.function || agg.func`
   deleted on both faces (ADR-0049 enforce-or-remove, #6321). The trailing
   `|| ''` on the remote face went with them: it only fired when neither key was
   written, and it made the two faces quote different spellings back.
3. Signatures narrowed.

One real behaviour change, not a type-only one. `groupBy` is a `GroupByNode`
union and the remote transport read it as `string[]`. The plain structured form
(`{ field: 'region' }`) is spec-valid AND pushed down by objectql today, and it
died here as `"[object Object]"` while the local face of the same driver
compiled it — a live remote/local fork. It now reads `.field`. A date-bucketed
entry genuinely cannot be compiled remotely and is refused with the ADR-0112
envelope (NOT_IMPLEMENTED/501). `SqlDriver`'s bare `throw` for the identical
condition took the same envelope in the same commit, so the two faces keep one
wire identity (#5240/#5907); a cross-package parity case compares their runtime
messages.

`alias` is deliberately not read, in step with `SqlDriver.aggregate`.

Closes #6321
@vercel

vercel Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 7, 2026 5:32pm

Request Review

@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/spec.

116 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/driver-turso, @objectstack/spec)
  • content/docs/deployment/environment-variables.mdx (via @objectstack/driver-turso)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/driver-turso)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql, @objectstack/driver-turso)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…0087 ledger (#6321)

The #6148 gate asks a declared-breaking changeset what the ledger disposition
is. Measured, rather than assumed: a candidate entry renders into the upgrade
guide's "Semantic (delegated to you, with acceptance criteria)" section as a
true, executable human instruction, and NOT into the "Mechanical (applied for
you)" table -- so it carries no `migrate meta` implication and names no stored
field that does not exist. That is the same disposition three existing entries
already carry for code surfaces: data-driver-find-stream-retired (#4484),
storage-service-list-retired (#5540), actor-user-roles-to-positions (#6011).

An untyped JS caller has no enforced channel at all once the alias is gone --
tsc reaches typed call sites only -- which is precisely the case the ledger
exists to reach.
@os-zhuang
os-zhuang marked this pull request as ready for review August 7, 2026 17:36
@os-zhuang
os-zhuang enabled auto-merge August 7, 2026 17:36
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 29e28a3 Aug 7, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6212-batch-b-aggregate-signatures branch August 7, 2026 18:02
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…stem-* migration entries states each lesson in words, not tracker numbers (stage 3) (objectstack-ai#20384)

Part of objectstack-ai#20233

Clause-②: no

**Stage 3 of a staged card.** The card stays open for later stages; this
PR carries no closing keyword. Text only: no entry id, `surface`, `from`
/ `to`, conversion or matching logic moves, and the chain rewrites
exactly what it rewrote before.

## What this does

`os migrate meta` prints every ADR-0087 semantic entry it crosses as one
block: `⚠ [protocol N] SURFACE → REPLACEMENT`, then `why:` (the entry's
`reason`) and `verify:` (its `acceptanceCriteria`). AGENTS.md's
runtime-string rule applies to all of it: 「Runtime strings — refusal
prose, prescriptions, anything an author is shown — carry no tracker
number (`pnpm check:doc-authoring`): the lesson goes into the text.」
Form **D** of ruling C+D on the parent card sets the shape: the lesson
in words, and no number, dead or alive.

This stage covers the next three families by site count, `driver-`,
`kernel-` and `system-`: **132 sites → 0** in the three prose fields.
None of the 26 entries carries a tracker id in `surface` (ruling A of
the stage-1 ACCEPT, `5858839916`, is checked and has nothing to do
here). Each site now says what the cited ruling, measurement or fix
decided. ADR ids stay. `registry.ts`, `spec-changes.json` and
`docs/protocol-upgrade-guide.md` are regenerated from the entries
(`gen:migration-registry`, `gen:spec-changes`, `gen:upgrade-guide`),
never hand-edited. The stage-1 pin now holds `engine-`, `ui-`,
`plugin-`, `driver-`, `kernel-` and `system-`.

## Census — tracker ids in the author-shown fields

**Instrument.** The stage-2 AST instrument, unchanged: a TypeScript-AST
walk over every `packages/spec/src/migrations/entries/**/*.ts`. For each
`entry` object literal it evaluates the string value of `replacement`,
`reason`, `acceptanceCriteria` and (counted separately) `surface`,
joining string literals with `+`, then counts `#` followed by 4 or 5
digits at a word boundary. **Validated first** by reproducing the
stage-1 readings on the stage-1 tree (`443b2f4fdc`, extracted with `git
archive`): `driver-` 7 entries / 44 sites (0 / 44 / 0, 25 distinct),
`kernel-` 9 / 44 (1 / 41 / 2, 11 distinct), `system-` 10 / 44 (0 / 42 /
2, 6 distinct), `engine-` 5 / 67, whole tree 266 entries / 1,016 sites /
9 `surface` sites — every figure equal to the stage-1 census. **Tree
measured:** `objectstack-ai/objectstack` at `569d4d2dbf` (this branch's
base). Unevaluable fields: 0.

**Controls, same run.**
- **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites
(replacement 1, reason 6), the reading stages 1 and 2 took.
- **Dark (comment lines):** 794 `//` lines in entry files carry a
tracker id, and none is counted. Comment lines belong to the sibling
card, and ⛔ this PR touches none (794 before and after).
- **Dark (field boundary):** the 7 `surface` sites left in the tree
(other families) count 0 in the three-field total and 7 in the `surface`
column.

**Re-measured on the base, matching the stage-1 census:** `driver-` 7
entries, **44** sites (replacement 0 / reason 44 / acceptanceCriteria
0), 25 distinct ids; `kernel-` 9 entries, **44** (1 / 41 / 2), 11
distinct; `system-` 10 entries, **44** (0 / 42 / 2), 6 distinct. 37
distinct ids across the three (the families share `objectstack-ai#14478`, `objectstack-ai#15939`,
`objectstack-ai#17635` and `objectstack-ai#3733`). `surface`: 0 in all three. Whole tree: 300
entries, **843** sites, 7 `surface` sites.

**After this PR:** `driver-` 0, `kernel-` 0, `system-` 0; `engine-`,
`ui-`, `plugin-` still 0; whole tree **843 → 711** sites; `surface` 7
(unchanged, other families).

| entry | sites (replacement / reason / acceptanceCriteria) |
|---|---|
| `17.driver-aggregate-undeclared-key-aliases-removed` | 6 (0 / 6 / 0) |
| `17.driver-capabilities-inert-bits-removed` | 4 (0 / 4 / 0) |
| `18.driver-options-timeout-to-timeout-ms` | 1 (0 / 1 / 0) |
| `17.driver-sql-distinct-bare-filter-typed` | 9 (0 / 9 / 0) |
| `18.driver-sql-unresolvable-where-column-refused` | 14 (0 / 14 / 0) |
| `18.driver-sql-upsert-cross-row-identity-merge-refused` | 9 (0 / 9 /
0) |
| `18.driver-turso-config-local-path-wasm-retired` | 1 (0 / 1 / 0) |
| `18.kernel-compatibility-matrix-estimated-migration-time-unit-in-key`
| 5 (0 / 5 / 0) |
| `18.kernel-context-preview-mode-retired` | 5 (1 / 4 / 0) |
| `18.kernel-event-bus-retention-unit-in-key` | 3 (0 / 3 / 0) |
| `18.kernel-health-check-and-hot-reload-durations-unit-in-key` | 7 (0 /
5 / 2) |
| `18.kernel-package-lifecycle-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.kernel-plugin-health-report-durations-unit-in-key` | 3 (0 / 3 / 0)
|
| `18.kernel-plugin-security-durations-unit-in-key` | 4 (0 / 4 / 0) |
| `18.kernel-runtime-config-timeout-unit-in-key` | 11 (0 / 11 / 0) |
| `18.kernel-startup-orchestrator-durations-unit-in-key` | 3 (0 / 3 / 0)
|
| `18.system-cache-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-collaboration-durations-unit-in-key` | 4 (0 / 4 / 0) |
| `18.system-failover-health-check-interval-unit-in-key` | 3 (0 / 3 / 0)
|
| `18.system-metrics-jsdoc-durations-unit-in-key` | 12 (0 / 12 / 0) |
| `18.system-metrics-window-durations-unit-in-key` | 5 (0 / 3 / 2) |
| `18.system-object-storage-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-registry-config-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-tracing-otel-exporter-durations-unit-in-key` | 5 (0 / 5 /
0) |
| `18.system-tracing-span-duration-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-worker-queue-rate-limit-duration-unit-in-key` | 3 (0 / 3 /
0) |
| **total, 26 entries** | **132 (1 / 127 / 4)** |

## Every citation read, and what the text now says

I read each cited issue or PR myself with single-card REST reads: the
body, and the comments where a ruling or a measurement lives. Ids are in
code spans so this body posts no cross-references. All 36 bare ids were
resolved against this repository, because every sentence that cites one
is about this repository's code; the one cross-repo id is `cloud#1651`.

| cited | what it decided (read) | how the text now carries it |
|---|---|---|
| `objectstack-ai#3733` | The pruned `cached` field key: measured, the parse succeeded
and the removed key was dropped without a word; the orphan schema was
deleted. | "an earlier field-key prune measured exactly that — the parse
succeeded and the removed key was dropped without a word" (health-check,
OTel exporter) |
| `objectstack-ai#3821` | The sharing-rule page: an unsortable query fell through to
an empty page, and the driver fix made an unsortable query lose its
ORDER BY, not its rows. | "the unknown-column recovery ladder (an
unsortable query loses its ORDER BY, not its rows)"; "the ladder's own
premise — rows matter more than their order"; "the ladder's recoveries"
|
| `objectstack-ai#4484` | `IDataDriver.findStream` removed: no production caller, two
of three implementations buffered the whole set, and no tombstone
because nothing parses a driver object. | "Retiring
`IDataDriver.findStream` (it had no production caller, and two of its
three implementations read the whole result set into memory …)";
"(`IDataDriver.findStream`, removed with no tombstone because nothing
parses a driver object)"; by entry id in the `distinct` entry |
| `objectstack-ai#4583` | The datasource ledger's dead keys removed; `capabilities.*`
went as a whole block (11 of 11 unread). | "was retired separately, as a
whole block nothing read" |
| `objectstack-ai#4634` | Audit of all 34 `DriverCapabilities` bits: 3 live, 31 dead
and tombstoned. | the entry already states the audit ("the follow-up
audit checked every bit"); the trailing id is dropped |
| `objectstack-ai#4914` | Maintainer, 2026-08-04: remove `manifest.loading` and
`PluginHotReloadSchema`; keep `HotReloadConfigSchema`, the side with an
implementation (`HotReloadManager`), as the start point. | "kept twice:
as the hot-reload vocabulary that had an implementation when the
manifest-side copy was removed, …" |
| `objectstack-ai#4984` | An org-axis red-line gate read only aliases the schema
rejects while its own fixtures spelt them: tests green, rule dead. |
"the family of the org-axis red-line gate that read only rejected
aliases while its own fixtures spelt them, so its tests stayed green and
the rule stayed dead" |
| `objectstack-ai#5181` | Narrow the query parameter of `IDataDriver`'s methods
(`DriverQuery`, no redundant `object`). | "neither the narrowing of
`IDataDriver`'s query parameters to `DriverQuery` nor the follow-through
…" |
| `objectstack-ai#5499` | Maintainer, 2026-08-05: freeze investment in `driver-memory`
/ `driver-mongodb`; fully lifted 2026-08-11 (comments `5249019855`,
`5252526378`). | "the maintainer's 2026-08-05 investment freeze on
driver-memory, which was lifted on 2026-08-11" |
| `objectstack-ai#5540` | Remove `IStorageService.list(prefix)`: zero consumers, and
the two adapters answered differently and both incompletely. | "(the
zero-consumer `IStorageService.list`, whose two adapters answered
differently and both incompletely)" |
| `objectstack-ai#6011` | Maintainer: close the `ctx.user` `roles` alias now. | "(the
`ctx.user` `roles` alias, closed at once on the maintainer's word rather
than given a window)" |
| `objectstack-ai#6075` | **404** — see Acceptance notes. | "the follow-through that
brought five drivers' implementations in line" |
| `objectstack-ai#6320` | `distinct`'s third argument meant different things on memory
and sql; the sql half was dispatched, the memory half held under the
freeze. | "(the measurement that found the two drivers reading this
argument differently split the fix: the sql half is this entry, and the
memory half was held back by that freeze)" |
| `objectstack-ai#6321` | `query.aggregate` / `agg.func` are undeclared aliases whose
only writers are driver fixtures; order: re-spell the fixtures, delete
the aliases, then narrow the signature. | "The removal ran in a fixed
order — the fixtures re-spelt first, the two alias branches deleted
second, the parameter narrowed to `DriverQuery` last — because the
reverse order yields red nobody can explain." |
| `objectstack-ai#6404` (PR) | Executed that order and narrowed `aggregate`'s query
parameter to `DriverQuery`. | the same sentence |
| `objectstack-ai#7929` | Maintainer, 2026-08-12, ruling B: `driver-sql`'s filter
refusal stops echoing `$field` operands, for every caller; the full
diagnostic goes to the server log. | "the same predicate-text disclosure
shape the driver's field-reference filter refusals had already been made
to stop echoing (the full diagnostic goes to the server log, never the
response)"; "that disclosure shape closed on the last dialect" |
| `objectstack-ai#8371` | Ruled option 2: a dotted filter key whose head is a
relation, a formula or a scalar is refused at both doors; a structured
head stays unjudged. | "the axis owned by the dotted-filter verdict,
which refuses a dotted key whose head is a relation, a formula or a
plain column at the protocol and engine doors" |
| `objectstack-ai#8592` | Measured on live MySQL: knex compiles the named conflict
target away. | stated by the entry ("knex drops the named keys before
the statement leaves the process"); the trailing id is dropped |
| `objectstack-ai#8621` | Option A: a pre-flight refusal when no unique index backs
the caller-named conflict target. | "Two earlier pre-flight refusals
closed the half where no unique index backed a caller-named target …" |
| `objectstack-ai#8622` | `id` becomes insert-only on the merge path: a merge on a
non-primary conflict key was measured rewriting the existing row's
primary key. | "`id` is insert-only on the merge path (made so once a
merge on a non-primary conflict key was measured rewriting the existing
row's primary key)" |
| `objectstack-ai#8755` | Ruling option A: a pre-flight refusal when a second unique
key could absorb a backed, caller-named target. | "… and the half where
a rival unique key could absorb a caller-named one" |
| `objectstack-ai#8790` | Maintainer, 2026-08-15: refuse both halves with
`INVALID_FILTER` / 400, naming the column. | "Ruled by the maintainer on
2026-08-15: refuse BOTH halves …"; "Recover-both was excluded by the
ruling's own argument" |
| `objectstack-ai#8807` | Maintainer, 2026-08-15: an upsert must never modify a row
whose identity the caller did not supply and whose conflict key it did
not name; enforcement delegated, blanket refusal excluded. | "Ruled by
the maintainer on 2026-08-15, as a contract principle …" (the principle
itself was already quoted verbatim) |
| `objectstack-ai#8926` | Maintainer, 2026-08-16, option A: MySQL's spelling joins the
one shared predicate (envelope and recoveries together). | "Addendum
2026-08-16." — the paragraph already states option A |
| `objectstack-ai#9061` (PR) | Implemented that option A. | the same |
| `objectstack-ai#11825` | Maintainer, 2026-08-25: retire the declarative
`AdvancedPluginLifecycleConfig` container; the classes stay a
host-driven library. | "… and as a host-driven library when the
declarative lifecycle config container was retired" |
| `objectstack-ai#11846` | **404** — see Acceptance notes. | "maintainer ruling
2026-08-27 (Option A: remove)"; "(as the removal ruling recorded)" |
| `objectstack-ai#14478` | Ruling B, 2026-09-02: a no-baseline gate plus an ADR-0087
rename of every offender (`DriverOptions.timeout` among the seven
named); ruling B again, 2026-09-05: the population is every authored and
every runtime-emitted duration, minus exemptions declared on the schema.
| "Maintainer ruling B on duration units (2026-09-02, its population
widened on 2026-09-05 to every authored and every runtime-emitted
duration, bar the exemptions a schema declares on the key itself)"; "the
duration-unit rule (…)" |
| `objectstack-ai#14519` | The two tenant timeouts published a describe naming no unit
(the unit sat in the JSDoc only); folded into the rename. | "the
unit-nowhere shape (no unit in the name or in the published describe,
first measured on two tenant timeouts)" |
| `objectstack-ai#15626` (PR) | Landed the gate and the seven founding renames, the
tenant `idleTimeout` → `idleTimeoutSeconds` among them. | "The tenant
half was already renamed, in the same change that landed the duration
gate itself" |
| `objectstack-ai#15678` | `kernel/`: the 14 remaining duration keys carry their unit
in the key name. | "the kernel-directory duration renames" / "the
kernel-directory round"; trailing ids dropped |
| `objectstack-ai#15679` | `system/`: the 15 remaining duration keys carry their unit
in the key name; `size` got an honest name. | "the system-directory
duration round"; trailing ids dropped |
| `objectstack-ai#15939` | The gate did not read JSDoc. Ruled 2026-09-07: refuse the
JSDoc / describe divergence. Ruled A 2026-09-11: remediate the
population per file first, land the widened gate last. | "Director-seat
ruling A of 2026-09-11 on the JSDoc-channel finding … a duration key
whose JSDoc names a unit its describe does not is refused, and the keys
in that shape are remediated per file before that refusal lands" |
| `objectstack-ai#16024` | Maintainer, 2026-09-06, per key: forward `timeout`, remove
`localPath` and `wasm`. | "ruled per key by the maintainer on
2026-09-06, once all three of this package's unread config keys had been
measured" |
| `objectstack-ai#17635` (PR) | The widened gate: refuse a duration key whose JSDoc
names a unit its describe does not; landed last. | "lands that widened
gate last, into a tree already clean" |
| `objectstack-ai#18669` | Ruling A, 2026-09-17: rename `FileValue.duration` and
`estimatedMigrationTime`, each with an ADR-0087 entry; no new closed
type, no narrowing of stored data. | "Maintainer ruling A of 2026-09-17
on the last two duration keys no closed duration type could express …" |
| `cloud#1651` | **Not readable from this session** — see Acceptance
notes. | "cloud — a census closed 2026-08-26: OS_PREVIEW_MODE there is a
routing-only switch …" |

No call-shaped token moves: a `name(` census over `registry.ts` is
identical before and after (297 distinct tokens), so textual
call-spelling ratchets read the same.

## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`,
widened

`COVERED_PREFIXES` is now `engine-`, `ui-`, `plugin-`, `driver-`,
`kernel-`, `system-`. The pin still spawns the real CLI (`os migrate
meta --from 16 --to 18`) once, locates each covered block **verbatim**
in stdout, and asserts the printed block — `surface` included — carries
no `#` plus 4 or 5 digits. Anti-vacuity:
- the `REWRITTEN` floor rises from 29 to **55** ids: the 26 entries of
this stage (7 `driver-`, 9 `kernel-`, 10 `system-`) are added, and every
covered prefix must still select at least one entry;
- presence in stdout is asserted before cleanliness (the
`driver-sql-unresolvable-where-column-refused` reason carries two
blank-line paragraph breaks, and its block is found verbatim);
- the detector is exercised on both sides first (lit on 4 and 5 digits,
dark on 3, 6 and `ADR-0112`).

The file keeps its stage-1 name; the header lists the six covered
families.

## Ablation — the widened pin can fail on a `driver-` block

From committed state, HEAD `1c0dc7ad54`, with
`scripts/ablation-replace.mjs` in wrap mode (it owns the restore trap)
and `scripts/ablation-dist-preflight.mjs` gating each leg. The bundle is
built from the generated `registry.ts`, so that is the file mutated.
- **Mutation.** In `registry.ts`, the reason of
`driver-sql-upsert-cross-row-identity-merge-refused`: anchor `pre-flight
refusals closed the half` → `pre-flight refusals (objectstack-ai#8621) closed the
half`. The tool read anchor 1 → 0 and replacement 0 → 1, blob `b41e1d44`
→ `8f8d226e`.
- **Mutate leg** (one lock turn: build, preflight, pin). Spec build exit
0. Preflight: marker present in 4 built files. Pin: **red**, `1 failed |
2 passed` — `driver-sql-upsert-cross-row-identity-merge-refused: the
printed guidance cites a tracker id: expected 'objectstack-ai#8621' to be undefined`.
- **Restore.** Tool-proven: blob `b41e1d44` == HEAD, `git diff HEAD`
empty.
- **Restore leg.** One lock turn, taken on the second try (the first
waited out its 540 s budget, exit 99, NOT MEASURED, the tree already
restored). Spec build exit 0. The `--absent` preflight found the marker
in none of 222 built files, with the working tree clean against HEAD.
Pin: **green**, `3 passed`.

## Verification

Final head **`1c0dc7ad54`** for every line below; each heavy run went
through `scripts/pm/os-verify-lock.sh` (one turn, `VERDICT command-exit
0`, per-step exits recorded separately).

- **Build:** `pnpm exec turbo run build --concurrency=2
--filter='@objectstack/cli^...'` gives `Tasks: 55 successful, 55 total`.
- **Pin and its neighbour:** `pnpm --filter @objectstack/cli exec vitest
run --project integration --maxWorkers=2
test/migrate-meta-engine-guidance.test.ts
test/migrate-meta-default-range.test.ts` gives `Test Files 2 passed`,
`Tests 10 passed | 1 skipped` (the skip is the default-range file's own
pre-existing `skipIf`).
- **Spec tests that read these entries or the registry:** `pnpm --filter
@objectstack/spec exec vitest run --maxWorkers=2 src/migrations
src/kernel/preview-mode-retirement.test.ts
scripts/build-schemas-check-mode.test.ts` plus the 19 other spec test
files that read `MIGRATIONS_BY_MAJOR`, the registry or an entry file:
`Test Files 24 passed`, `Tests 696 passed`.
- **CLI unit:** `test/vitest-tiers-partition.test.ts` and
`src/utils/spec-release-changes.test.ts`: `Test Files 2 passed`, `Tests
28 passed`.
- **The call-spelling census that reads `registry.ts`:** `pnpm --filter
@objectstack/driver-sql exec vitest run --maxWorkers=2
src/sql-driver-query-signature.test.ts` gives 15 passed.
- **Typecheck:** `pnpm --filter @objectstack/spec typecheck` exits 0
(test layer: 53 files / 255 errors held in its ledger); `pnpm --filter
@objectstack/cli typecheck` exits 0 (test layer: 3 files / 28 errors
held, unchanged).
- **Gate families:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derives **89** families at
`1c0dc7ad54` (after `git fetch origin main`). `--ran` over the recorded
exit codes reads **89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN**, all
exit 0. They include `check:doc-authoring` ("16466 customer-facing
string(s) across 1135 spec sources clean"), `check:issue-citations`,
`check:migration-registry` ("registry.ts is current (300 semantic, 219
retired-key, 199 retired-def)"), `check:spec-changes`,
`check:upgrade-guide`, `check:generated` ("All 15 generated artifacts
are up to date"), `check:duration-unit-keys`, `check:nul-bytes`,
`check:adr-0087-registration` and `check:changeset-no-major`.
- `check:dual-build-cjs-loads` refused first with `PREREQUISITE NOT MET`
(exit 3: twelve packages outside the CLI closure had no `dist/`). Those
`dist/` directories were written later in the same pass (04:48–04:49Z,
inside the `check:type-check-debt` run, whose re-measure builds them);
re-run at the same head it exits 0 (104 entries / 66 packages / 659 CJS
files). The reconciled list takes that latest run.
- **Lint (a proven narrowing, not the repo-wide run, which is CI's):**
`eslint --no-inline-config --format json` over the 28 changed `.ts`
files reports 28 files, 0 errors, 0 warnings.
- The population is read from `eslint.config.mjs`:
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 28
are in it (no file-ignored warning).
- Invariance: the config enables no type-aware linting (no
`parserOptions.project`, no typed rules), so a text edit cannot move the
verdict on a file it does not touch.
- **Mergeability:** see Acceptance notes (driver-free `merge-tree`
against `862b6ce869` exits 0).

## Acceptance notes

- **Two dead ids, rewritten from the code on `main`.** `objectstack-ai#6075` and
`objectstack-ai#11846` answer 404 on both the issues and the pulls endpoint, re-probed
with a 200 control (`objectstack-ai#14478`).
- `objectstack-ai#6075` (`distinct`'s "never reached it" sentence):
`packages/drivers/driver-sql/CHANGELOG.md` (commit `d367f03`) and
`sql-driver-query-signature.test.ts` record what it did — the five
drivers' implementations followed `IDataDriver`'s `DriverQuery`
narrowing. The sentence now says exactly that.
- `objectstack-ai#11846` (preview mode): `packages/spec/CHANGELOG.md` (commit
`0c2334f`), `packages/spec/src/kernel/context.zod.ts` and
`preview-mode-retirement.test.ts` record the 2026-08-27 ruling (Option
A: remove), the three-repo zero-consumer measurement and the
re-declare-fresh condition. Dropped because `main` does not state them:
"decision-inbox batch 2" and "all four decision facets pointed the same
way". "The objectstack-ai#11846 card records the measurement" (objectui leg) now reads
"zero consumers, measured when the removal was ruled", which is what the
changelog and the test header say.
- **One cross-repo id this session cannot read.** `cloud#1651` answers
403 here: `objectstack-ai/cloud` is not attached to this session
(`add_repo` refused: no access). It is neither confirmed nor refuted, so
its sentence was rewritten from what `main` records about it —
`packages/spec/CHANGELOG.md` (`0c2334f`: closed 2026-08-26 with positive
controls, `RuntimeMode` zero hits, `ArtifactKernelFactory` 20+ hits and
never touching `previewMode`) and `context.zod.ts` (`OS_PREVIEW_MODE`
there is routing-only). The cloud-side detail `main` does not state —
`previewMode` "only as a local variable" whose effect is adding
wildcards to "CSRF" trusted origins — is dropped; the parenthesis that
replaces it describes this repository's own `serve.ts` (the one reader
of `OS_PREVIEW_MODE` here only widens better-auth's trusted origins to
preview-domain wildcards), which is measured on `main`.
- **Decision-batch numbers went too (invisible to the regex).** Nineteen
sites cited a decision batch as `#` plus two or three digits (`objectstack-ai#43` ×13,
`objectstack-ai#115` ×4, `objectstack-ai#151` ×1, `objectstack-ai#158` ×1). They are numbers an author is shown
and cannot follow, so each is dropped. One consequence worth naming: 13
entries said `Maintainer ruling B on objectstack-ai#14478 (2026-09-02, decision batch
objectstack-ai#43)`, which fused two rulings on the same card — B of 2026-09-02 (the
gate and the no-baseline rename) and B of 2026-09-05, decided in that
batch (the population: every authored and every runtime-emitted
duration, minus schema-declared exemptions). The sentence now names both
dates. The `objectstack-ai#158` sentence (the agreement shape ruled an offence on
2026-09-18) is corroborated by
`.changeset/18075-agreement-shape-is-an-offence.md` on `main`.
- **"issue NNNN" / "PR NNNN" spellings, checked by hand.** No
bare-number spelling exists in these 26 entries' author-shown text; the
two `PR` citations (`PR objectstack-ai#6404`, `PR objectstack-ai#9061`) were `#`-spelled, so the
instrument saw them and they are gone. The only `#` left in these 26
files is on `//` comment lines (sibling card's surface), including a
`Prime Directive objectstack-ai#13` reference.
- **A citation whose page says something narrower than the text.**
`kernel-health-check-and-hot-reload-durations-unit-in-key` called
`shutdownTimeout`'s shape "the objectstack-ai#14519 unit-nowhere shape". `objectstack-ai#14519`'s
keys carried their unit in the JSDoc; "unit nowhere" is the gate's name
for it (`check-duration-unit-keys.ts` header: "no unit ANYWHERE (the
objectstack-ai#14519 shape)"), because the gate did not read JSDoc. The sentence now
says what the shape is — no unit in the name or in the published
describe — and that it was first measured on two tenant timeouts.
- **A comment that my text edit makes slightly stale.**
`18.system-metrics-window-durations-unit-in-key.ts` carries a `//`
comment saying its acceptanceCriteria sentence "is objectstack-ai#15679's, left word
for word". That sentence now says "that JSDoc-channel gap is filed as a
finding of its own" where it said "is objectstack-ai#15939": same content, no number.
The comment is the sibling card's surface (comment lines), so it is
untouched here.
- **Three "card" references re-anchored.** Removing an id left "the same
card" in the Turso entry pointing at nothing; it now says "the same
measurement". The kernel entries' "renamed by this same card" carry no
number and were not otherwise rewritten, so they are left.
- **Cross-PR check: no open PR adds or edits a `driver-`, `kernel-` or
`system-` semantic entry.** Read at 2026-09-28T04:0xZ: the 18 open PRs'
file lists (`GET /pulls/{n}/files`) carry 0 files matching
`migrations/entries/semantic/NN.(driver|kernel|system)-*`. The Version
Packages PR (`objectstack-ai#17076`) lists more than 1,000 files; the 1,100 rows read
carry no entry file, and it is the bot-generated release PR. Nothing in
flight will be held by the widened pin on arrival.
- **`main` moved 4 commits past the base** (`862b6ce869`: `objectstack-ai#20364`,
`objectstack-ai#20341`, `objectstack-ai#20366`, `objectstack-ai#20352`); none touches
`packages/spec/src/migrations/` or the pin. A driver-free bare-clone
`merge-tree --write-tree` of this head against `862b6ce869` exits 0 with
no conflicted path, so `registry.ts` needs no merge, and `main` was not
merged in.
- **Generated projections** (`spec-changes.json`,
`docs/protocol-upgrade-guide.md`) are regenerated, as in stages 1 and 2;
their `--check` legs are green. Only the three `driver-` entries
registered at protocol 17 appear in them, which is why those diffs are
small.
- **No other test pins these entries' text.** A `git grep` of test files
for the 26 entry ids finds one (`preview-mode-retirement.test.ts`),
which names the entry in a comment and reads no prose; a grep of tests
for the 37 cited numbers finds only comment lines. So no test needed
re-pinning this stage (stage 2's `migrations.test.ts` case has no
counterpart here).

## Line budget

Entry files: **352 changed lines** (+228 / −124) across 26 files,
against the stage-1 ≈400 budget. The whole diff is **776 lines** (+516 /
−260) in 31 files. Of the rest, `registry.ts` is 352, the two
projections are 18 (`spec-changes.json` 12, the upgrade guide 6), the
widened pin is 33 and the changeset is 21.

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

---------

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

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] query.aggregate 与 agg.func 是 spec 从未声明的宽容别名,唯一书写者是驱动自己的 fixture(#4984 家族)

1 participant