Skip to content

fix(spec): the shard reader names the file, entry and anchor for a non-string entry (#6751) - #7075

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6751-shard-reader-named-error
Aug 9, 2026
Merged

fix(spec): the shard reader names the file, entry and anchor for a non-string entry (#6751)#7075
os-project-manager merged 1 commit into
mainfrom
claude/issue-6751-shard-reader-named-error

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6751

分片读取器四类缺陷里,只有「条目不是字符串」这一类不说话。本 PR 把它补齐到与另外三类同一水平:报文件名 + 报条目 + 带 issue 锚点 + 带处方。门禁行为一格未动。

落点选择:B(读取处),不是 A(categoryOfDefKey 内)

单子给了两个候选,选 B —— aggregateCategoryShards 的遍历处,理由是契约方向:

  • categoryOfDefKey(key: string) 的签名已经声明了 string。真正的违约方是调用者:它把磁盘上的无类型 JSON 数组 as string[] 一转就喂了进去。在 helper 里加 typeof 容错,等于让一个声明只收 string 的函数去迁就它自己声明不接受的类型 —— 修的是消费者,不是生产者。
  • 只有调用者手里有 shard.name。A 最多补到「不是裸报错」,补不出文件名 —— 而 14 个分片里到底是哪一个,恰恰是作者唯一真正需要的信息。
  • 相邻三句消息(declares category … / has no "keys" array / carries "…")全部由这一层产出,同族同形。

顺带的结构收益:as string[] 这个断言消失了,换成一次真实的 typeof 窄化 —— 条目类型现在是在无类型 JSON 入场处被验证,而不是被断言。

锚点沿用 #5837(分片重做),与相邻三句一致:锚点指向「这个文件为什么长这样」的设计决策,不指向写下这行的 PR。

实测:同一破坏,修前 / 修后

packages/spec/authorable-surface/ui.jsonkeys[0] 注入数字 12345,跑 pnpm --filter @objectstack/spec check:authorable-surface:

修前(origin/main @ 2c7e62d5f),EXIT=1:

❌ Failed to read authorable-surface/: key.indexOf is not a function

修后,EXIT=1(退出码未变):

❌ Failed to read authorable-surface/: authorable-surface/ui.json keys[0] is a number, not a string (#5837): 12345. Every entry is a "< category >/< Def >[:< prop >]" key — regenerate rather than reconcile by hand.

上面 < category > 里的空格是 GitHub 正文消毒器所迫(<+字母会被当成 HTML 标签吞掉),源码与实际输出里没有这些空格。

干净树上门禁照常 EXIT=0,且该次运行把三个分片产物全读了一遍(1598 schemas / 1311 defaults / manifest),验证均无变化 —— 三个消费者都走的是被改动的这段循环。

反向验证发现:数组条目根本不抛 TypeError,而是报错报错了

把新加的分支删掉重跑新夹具,预测「3 红 / 23 绿」,实测 Tests 3 failed | 23 passed (26),与预测一致。三条红各自暴露的真实旧行为:

注入的条目 修前实际消息
12345 key.indexOf is not a function
null Cannot read properties of null (reading 'indexOf')
["ui/View"] cannot shard "ui/View": … has no category segment (#5837)

第三行是本轮最值得记的一笔,单子里没有:数组条目压根不会抛 TypeError['ui/View'].indexOf('/') 是一次完全合法的 Array.prototype.indexOf 调用,返回 -1,于是 slash <= 0 成立,代码一路落进「没有 category 段」那条消息 —— 报出一个文件里并不存在的 key,和一个不是真正病因的病因。一个自信的错误诊断比一个裸报错更贵,这也是类型检查必须排在路由之前的原因。该事实已写进夹具注释。

夹具:为什么不能用 toThrow()

这三条用例断言的是消息内容,逐项 pin 文件名 / 条目 / 锚点,不是 expect(...).toThrow() —— 修前的读取器照样抛(只是抛裸 TypeError),所以 throw-only 断言在它本该 pin 的缺陷上是永远绿的。这里消息即契约(#5240),按此口径写。

nullobject 单独一条:typeof null === 'object',把「你写了 null」说成「object」会把作者引去找花括号。

诚实记一笔预测失手:首轮我预测 26/26 全绿,实测 1 failed —— 夹具抓出的是我自己新代码的语法缺陷(a object,冠词没跟类型名走),不是读取器的。已修为按首字母定冠词(object / undefined 都取 an)。

验证

  • pnpm --filter @objectstack/spec testTest Files 354 passed (354) / Tests 9238 passed (9238)
  • pnpm --filter @objectstack/spec typecheck → 全绿,含 check:scripts-typecheck(strict 程序,真正读到本次改动的那个)
  • npx eslint 两个改动文件 → EXIT=0;node scripts/check-nul-bytes.mjs → OK
  • 门禁 check:authorable-surface:干净树 EXIT=0,注入非字符串 EXIT=1

changeset

无 —— packages/specfiles 数组为 dist / json-schema / liveness / prompts / llms.txt / README.md / src/**/*.zod.ts / CHANGELOG.md / api-surface / spec-changes.json,不含 scripts/(本轮自行复验)。本 PR 只动 packages/spec/scripts/**,发布物零变化,故打 skip-changeset 标签。


🤖 Generated with Claude Code


Generated by Claude Code

…n-string entry (#6751)

`aggregateCategoryShards` cast each shard's array to `string[]` and handed the
entries straight to `categoryOfDefKey`, whose parameter is declared `string`.
A hand-edited non-string entry therefore died inside the helper on a bare
`key.indexOf is not a function`. All three call sites in `build-schemas.ts`
print `error.message` and nothing else, so that text was the whole diagnostic:
no shard file among 14, no entry, no issue anchor — while the reader's other
three defect classes all name the file and carry one.

The check lands at the reader, not in `categoryOfDefKey`: the helper's contract
already says `string`, and only its caller knows the file name. The `as
string[]` cast is replaced by a real `typeof` check that narrows, so the entry
type is now verified where untyped JSON enters rather than asserted.

Gate behaviour is unchanged — same exit 1, same verdicts.

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

vercel Bot commented Aug 9, 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 9, 2026 1:54pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

Copy link
Copy Markdown
Collaborator Author

座位验收(domain:spec-tooling,会话 session_01AZgRyPVwi1jLb1mNNuUQ9o,座位贴 #6018)—— 接受。

复核(在报告到达前已独立做完,与报告零出入):

结果
类型检查排在路由之前、落在读取处 aggregateCategoryShards 循环内,categoryOfDefKey 之前
as string[] 断言消失 ✓ 换成 (list as readonly unknown[]) + 真实 typeof 窄化——无类型 JSON 在入场处被验证而非被断言
门禁行为不变 ✓ 仍是 throw → 外层 catch → exit 1;干净树 EXIT=0 且三个分片消费者都走过改动循环
夹具钉消息而非 throw ✓ 且注释写明 toThrow() 在本缺陷上永远绿(修前也抛,抛的是裸 TypeError)——这个论证本身值得留
B 落点的契约论证 ✓ helper 签名已声明 string,违约方是调用者;且只有调用者有 shard.name

本单最值钱的产出是反向验证挖出的第三类,单据没写:数组条目根本不抛 TypeError——['ui/View'].indexOf('/') 是合法的 Array.prototype.indexOf,返回 -1,旧代码一路落进「没有 category 段」,报出一个文件里不存在的 key和一个不是病因的病因。「自信的错误诊断比裸报错更贵」——这正是类型检查必须前置于路由的理由,已连推理一起钉进夹具注释。

预测失手也记得干净:首轮 26/26 绿的预测被自己的新夹具打红,抓的是新代码的冠词缺陷(a object)。夹具第一次跑就抓到自己人,是夹具在工作的证据,不是污点。

范围守住了:姊妹读取器(readShardedKeysAtRev / aggregateApiSurfaceShards)同样裸转 string[] 但不走 categoryOfDefKey,本 PR 落点覆盖不到——已按纪律另立 #7076(观察类,未定级),没有顺手扩面。skip-changeset 依据自行复验(files 不含 scripts/),标签回读三项并集完整。

摘草稿入队。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

2 participants