Skip to content

观察:build-schemas.ts 读 authorable-surface.json 的 JSON.parse 无保护 —— 手编成非法 JSON 时抛栈而非给处方(与姊妹 reader 不对称) #5873

Closed as not planned

Description

@baozhoutao

范围外发现,记录于 #4663 的前提复验过程中(该单前提已被证伪,详见其评论)。观察类:不构成绕过,门禁照样红,只是诊断质量与同文件的姊妹路径不对称。

事实

packages/spec/scripts/build-schemas.ts 里有两个「读一个受字节纪律约束的committed 产物」的路径,保护程度不同:

读取对象 位置 JSON.parse 保护 形状校验
authorable-surface.base.json(锚点) readCommittedSurfaceBase() L951–982 有:try/catch → ❌ ... is not valid JSON (#5235) 有:baseRev 40-hex + Array.isArray(keys),并给出 git checkout -- / 重锚两条处方
authorable-surface.json(本体) L595–598 无 无(L602 直接 surfaceDoc.keys.map(...))
// build-schemas.ts:595-598
if (fs.existsSync(AUTHORABLE_SURFACE_PATH)) {
  surfaceRaw = fs.readFileSync(AUTHORABLE_SURFACE_PATH, 'utf-8');
  surfaceDoc = JSON.parse(surfaceRaw) as AuthorableSurface;   // ← 未保护
}

实测

在 e2bfa6ce3(当日 origin/main)的 worktree 里,把 authorable-surface.json 末尾的 } 删掉(模拟一次手编翻车),跑 pnpm --filter @objectstack/spec check:authorable-surface:

EXIT=1
SyntaxError: Expected ',' or '}' after property value in JSON at position 311133 (line 7942 column 1)
    at JSON.parse (anonymous)
    at path (/…/packages/spec/scripts/build-schemas.ts:597:21)

即:退出码正确(1),但输出是一条 node 栈,而不是这套门禁一贯的「一句诊断 + 一句修法」。同样的破坏打在 .base.json 上,得到的是 ❌ authorable-surface.base.json is not valid JSON (#5235) 加两条处方。

keys 被改成非数组 / 含非字符串项时同理:L602 的 .map(...) / .replace(...) 抛 TypeError,红但无处方。

为什么归为观察类

建议修法(窄)

把 L595–598 对齐 readCommittedSurfaceBase():try/catch + Array.isArray(doc?.keys) 形状校验,失败时输出与字节比对分支(L1536–1546)同族的处方 —— 都是 pnpm --filter @objectstack/spec gen:schema。不需要新增任何语义,只是让这条路径和它的姊妹一样会说话。

关联:#4650(门禁本体)、#5235(锚点 reader,对照实现)、#4663(本发现的来源单)

Activity

  1. claude commented on Aug 6, 2026

    @claude
    Contributor

    发现分诊轮(#4949 纪律):晋级 pm:queue,摘 finding。域维持 domain:spec。

    • 过时前提检查(origin/main 9e3709a)—— 前提成立,且比正文更强。 正文写的 L595–598 已漂到 packages/spec/scripts/build-schemas.ts:665(surfaceDoc = JSON.parse(surfaceRaw) as AuthorableSurface;),紧随其后 :670 仍是无保护的 surfaceDoc.keys.map(...)。本轮实读发现该文件现在有三个 surface JSON 读取面,受保护的从一个变成两个::1079(.base.json 锚点)与 :1209(baseRev 那份 upstream 副本,❌ … is not valid JSON (#5235))都有 try/catch + 处方,唯独工作树本体那一读没有。⇒ 正文的「与姊妹 reader 不对称」在本轮不是二比一,而是二比一里唯一的例外,论证方向未被削弱、只被加强。
    • 为什么晋级(不再按观察类持有):入队门槛按 step 0 的 scoped tooling/gate fix 一条即足 —— 落点唯一(一个文件、两行 + 一个形状校验)、参照实现就在同文件(readCommittedSurfaceBase(),正文给了 L951–982 的锚点)、处方字符串复用既有的 pnpm --filter @objectstack/spec gen:schema,零语义新增、零取舍。这类单不需要裁决,只需要有人做。
    • 不改判它今天无害:正文的边界判断本轮核过并同意 —— 未捕获异常在 node 下即 exit 1,--check 在解析前不写盘,故既不过绿也不改树(check:authorable-surface 在 --check 模式下仍会写 json-schema.manifest.json —— 一个「检查」在改工作区 #4711 未退步)。晋级的理由不是「有人在踩」,而是「修法已知、成本封顶、且这条分支是同一套门禁里唯一不说话的那条」。
    • 查重:三仓 open issue / PR 无影子单;#4650(门禁本体)、#5235(锚点 reader,对照实现)、#4663(来源单,其自身前提已被证伪)均为关联而非重复。⚠️ 无在飞 PR 触 build-schemas.ts(本轮核过 31 条 objectstack open PR),入队即可派,无串行约束。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Lane correction: domain:spec → domain:spec-tooling (maintainer-authorized relabel pass, 2026-08-07; PM session acting as the sanctioned triage channel for this pass — domain:* otherwise has a single producer).

    Criterion applied (anchoring rule — the domain is the package/face the fix lands in, read from the body, not the title): the landing site is packages/spec/scripts/build-schemas.ts, i.e. the generator/gate machinery around the contract, which is the domain:spec-tooling scope (#6018 / #5163). It touches no *.zod.ts and changes no accepted metadata — the fix is aligning one reader's error handling with its sibling readCommittedSurfaceBase().

    pm:queue unchanged.


    Generated by Claude Code

  3. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    认领 · domain:spec-tooling 座位派单 — 会话 session_01AZgRyPVwi1jLb1mNNuUQ9o(座位贴 #6018),分支 claude/issue-5873-surface-read-reanchor,pm:queue → pm:dispatched。

    派的是「重锚」,不是「按正文行号去修」 —— 这一点必须写清楚,因为本单正文与 08-06 分诊评论指认的落点行号,已被 01:05Z 另一座位的派发前核验判定为全部失效(build-schemas.ts 经 #6398 / #6360 / #6461 多轮重做)。我复核并同意该判定,它仍然成立,所以照正文派单就是一次必然的空派。

    ㉔ 落点复查(14:1xZ):packages/spec/scripts/build-schemas.ts 最新提交 747d2e45(#6452 via PR #6461,08-07T23:48Z),此后零改动 —— 即那次重做已经稳定近 14 小时,现在正是重锚的时机,而不是继续挂着。

    本次派单的交付物是一个判定,不是一个必然的补丁:在重做后的代码上重新定位「工作树本体 surface 的读取路径是否仍存在无保护的 JSON.parse 分支」。

    • 若仍存在 → 按姊妹 reader readCommittedSurfaceBase() 的既有形状对齐(try/catch + 处方),这就是正文本来要的那个修法;
    • 若已不存在(被重做顺带修掉,或读取路径已改走带 error 返回的 readShardedKeysAtRev)→ 报 premise_still_valid: false 并附证据即可,不要为了交差硬造一个补丁。那同样是一次成功的派发,单据按 not planned / completed by rework 关闭。

    竞态复核:线程内为 08-06 分诊、01:05Z 的退回记录、01:06Z 的域标签更正与本条,无他座位认领。不相交:本批在飞的 #6713+#6716 落在 packages/lint/src/validate-expressions.ts、#5757 落在 check-generated.ts、在队的 #6738 落在 format-type.ts,与本单文件面均无交集。


    Generated by Claude Code

  4. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    重锚结论:前提已被证伪 —— 本单描述的无保护读取在 main 上不复存在,不提 PR。

    会话 session_01AZgRyPVwi1jLb1mNNuUQ9o,分支 claude/issue-5873-surface-read-reanchor(未推,已删)。worktree @ origin/main 252f71bd6,build-schemas.ts @ 747d2e451(与派单 ㉔ 复查一致)。

    一句话

    正文的主体不是「行号漂了」,而是读取对象本身没了:packages/spec/authorable-surface.json 这个文件已在 #5837(PR e3ef52b5a / #6069)被拆成 packages/spec/authorable-surface/ 下 14 个 category 分片,工作树本体的读取改走 aggregateCategoryShards,该路径双层受保护,且实测输出与姊妹 reader 同形。

    现状枚举:build-schemas.ts 内全部产物读取面

    readFileSync 全文仅 2 处,JSON.parse 全文仅 3 处,逐条:

    位置 读取对象 保护
    :217 本包 package.json 取版本号 无 try/catch —— 但读的是 npm 清单,不是受字节纪律约束的门禁产物,不在本单射程
    :503 schema-manifest/ try/catch → ❌ Failed to read schema-manifest/: ... + exit 1
    :733 authorable-surface/(工作树本体,即本单主角) try/catch → ❌ Failed to read authorable-surface/: ... + exit 1
    :1127–:1130 authorable-surface.base.json(锚点) try/catch → ❌ ... is not valid JSON (#5235)(正文的对照实现)
    :1510 git show 出的上游锚点副本 try/catch → 返回 null(注释写明「deliberately quiet」)
    :2324 authorable-defaults/ try/catch → ❌ Failed to read authorable-defaults/: ... + exit 1

    内层还有一层:lib/sharded-artifacts.ts 的 readShards() 把每个分片的 JSON.parse 单独包了 try/catch,转成具名 Error;aggregateCategoryShards() 另做 category 与文件名一致性、Array.isArray(list)、逐条 category 归属三项形状校验。即正文「建议修法」要的 try/catch + Array.isArray(doc?.keys) 两件事,现在都已存在,且比建议的更细(能报到具体分片)。

    实测(复刻正文的实验,四组,均在本轮 worktree 内做完即还原)

    # 破坏方式 预测(跑之前记录) 实测
    A 不破坏 exit 0 EXIT=0,✅ Successfully generated 1596 schemas.
    B 删掉 authorable-surface/ui.json 末尾 } 正文预测「裸栈」;我按静态阅读预测「被捕获的 ❌ 单行」 EXIT=1 — ❌ Failed to read authorable-surface/: authorable-surface/ui.json is not valid JSON (#5837): SyntaxError: Expected ',' or '}' after property value in JSON at position 34144 (line 1035 column 1) 无 at JSON.parse 栈帧
    C keys 改成非数组 正文预测「.map() 抛 TypeError,红但无处方」 EXIT=1 — ❌ Failed to read authorable-surface/: authorable-surface/ui.json has no "keys" array (#5837). 无 TypeError
    E 对照组:同样破坏姊妹 .base.json ❌ ... is not valid JSON (#5235) EXIT=1 — ❌ authorable-surface.base.json is not valid JSON (#5235): SyntaxError: ... at position 308089 (line 7842 column 1)

    B 与 E 并排就是本单的判据:同形,且 B 比 E 更精确 —— E 只能报它唯一会读的那个文件名,B 从 14 个分片里点出了 ui.json。

    一处要更正正文(顺带测出来的)

    正文说姊妹 reader 在非法 JSON 时给的是「❌ ... 加两条处方」。实测 E 显示:姊妹 reader 的 JSON.parse 分支本身也只有一行 ❌,没有处方 —— readCommittedSurfaceBase() 里那两条 git checkout -- / 重锚处方挂在它的形状校验分支和字节比对分支上,不在 JSON 解析分支上。所以正文设定的「对称基准」当时就略高于姊妹的实际水平;按姊妹的真实水平衡量,工作树本体这一路今天已经追平并略微超出。

    边界结论(与正文一致,未改判)

    仍然无害:三个调用点都是 console.error + process.exit(1),退出码正确;--check 在解析前不写盘,#4711 纪律未退步。区别只是:当初「唯一不说话的那条分支」现在会说话了。

    残留(已另开单,不在本单修)

    四类破坏里唯一还没追平的是「keys 内注入非字符串条目」:实测 EXIT=1 — ❌ Failed to read authorable-surface/: key.indexOf is not a function,被外层 catch 兜住(无栈、退出码对),但不报文件名/条目/锚点。落点在 lib/sharded-artifacts.ts 的 categoryOfDefKey,且为三个分片产物共用 —— 属于「其它文件的保护缺口」,按派单纪律另行开单不在本单修:已开 #6751(观察类,finding,未指派)。

    建议本单按 not planned / completed by rework 关闭。


    Generated by Claude Code

  5. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    关闭 —— 前提已消失(not planned / completed by rework)。 domain:spec-tooling 座位重锚派单的结果,零代码改动。

    漂移比「行号失效」更深:读取目标本身没了

    01:05Z 的退回记录说的是行号锚点失效。实测下来是更彻底的一件事:packages/spec/authorable-surface.json 这个文件已经不存在了 —— #5837(commit e3ef52b5a / PR #6069)把它按 category 拆成了 packages/spec/authorable-surface/ 目录下的 14 个分片。本单描述的那个「读单体 JSON 再无保护 JSON.parse」的动作,今天在代码里没有对应物。

    工作树本体的读取现在是 aggregateCategoryShards(AUTHORABLE_SURFACE_DIR, 'keys'),两层保护:外层 try/catch 打 ❌ Failed to read authorable-surface/: … 并 exit 1;内层 lib/sharded-artifacts.ts 的 readShards() 把每个分片的 JSON.parse 包成具名 Error,aggregateCategoryShards() 另做三项形状检查,含本单点名要的 Array.isArray 那一项。

    座位独立复核(未采信报告,origin/main 实读):packages/spec/authorable-surface 确为目录;build-schemas.ts 全文 JSON.parse 恰好三处 —— :217(package.json,npm 清单,非字节纪律产物)、:1130(base 锚点,带 #5235 处方)、:1510(git show 输出,返回 null);readFileSync 两处,均在上述之内。无保护的 surface 读取一处不存。

    四轮反向验证(方向均在运行前预判)

    开发者把本单自己的预测当假设来检验,结果是单据被证伪的那一半:

    变异 单据预测 实测
    B:ui.json 去掉尾括号 裸 node 栈 ❌ Failed to read authorable-surface/: authorable-surface/ui.json is not valid JSON (#5837): SyntaxError …,零 at JSON.parse 栈帧
    C:keys 换成非数组 裸 TypeError ❌ … has no "keys" array (#5837).
    E(对照):同样的破坏施于姊妹 authorable-surface.base.json — ❌ authorable-surface.base.json is not valid JSON (#5235): …

    E 这条对照是关键:B 与 E 并排看,形状相同,而 B 更精确(它点名 14 个分片里坏的是哪一个,姊妹只读一个文件、无从点名)。所以不仅不对称已经消失,工作树这一侧还略优于当初被当作标杆的那一侧。

    顺带更正本单的一处论证前提

    单据把姊妹 readCommittedSurfaceBase() 当作「带处方」的标杆。实测:姊妹的 JSON.parse 分支同样只打一行 ❌、没有处方 —— 它的两条处方挂在形状检查与字节比对分支上。也就是说本单当年设定的对称基线,略高于姊妹的真实行为。这不改变结论(不对称已消失),但记录下来,免得日后有人照这段论证去「补齐处方」而发现无物可补。

    一处诚实未命中 → 已另立单 #6751

    开发者额外测了单据点名的另一个用例(keys 里混入非字符串项),预判会给出具名消息,实测没有:❌ Failed to read authorable-surface/: key.indexOf is not a function —— 捕获了、退出码对,但既不点名文件、也不点名条目、也无 issue 锚点,与同一读取器里另外三类缺陷的处理不一致。修法落点在 lib/sharded-artifacts.ts 的 categoryOfDefKey,为三个分片产物共用 —— 正是本次派单明确划在范围外的「其他文件里的保护缺口」,故立单不修(#6751,finding,未定级、未指派,交分诊)。这一条是本次派发真正的增量:它不在任何人原先的视野里。


    Generated by Claude Code

  6. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Triage state repair: closed (not planned / completed by rework) — executing the disposition already recorded on this thread.

    The domain:spec-tooling seat's 14:15Z re-anchor report and its 14:16:54Z acceptance both concluded the premise no longer exists on main (the monolithic authorable-surface.json was sharded by #5837/PR #6069; the working-tree read is now double-protected and measured stronger than the sibling baseline), and announced closure — but the close never landed, leaving the card in a label half-state (domain:spec-tooling with no pm-state) that the 14:33Z triage wrap-up flagged for exactly this repair if still open this fire. Verified at 17:2xZ: still open, no pm-state, no further activity after 14:16:54Z ⇒ orphaned half-state, closed per the recorded disposition. The one residual gap found during re-anchoring is already filed separately as #6751 (finding, awaiting grading). Zero code changes in this action.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

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

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions