Skip to content

strictObject 的 history 句夹在「哪个键错了」与「该写什么」之间,在单行 error 显示位上把修法推到 222 字符之后(#5762 实测) #5955

Description

@os-zhuang

#5762 把 flow-time-relative-descriptor-invalid 从 warning 升为 error 时实测出来的,记录防丢。未指派,交分诊定级。 本条是 #5762 第三问(「zod history 句在 error 显示位的噪音」)里未在该 PR 内解决的那一半 —— 裁定允许折叠,但干净的修法不在那个 PR 的文件面内(见下)。

测量

strictUnknownKeyError 的消息结构是(packages/spec/src/shared/suggestions.zod.ts:320):

Unrecognized key(s) on <surface>: <keys>. <history>   ← 拼在这里
[ Did you mean `k` → `canonical`? ]                    ← 修法在最后
[ \n  • <guidance 逐条> ]

history 是每个 surface 自己声明的一句沿革。以 TimeRelativeTriggerSchema 为例,它是 222 字符:

Until #4001 these were dropped silently — the descriptor still parsed and the sweep still bound, so a mis-spelled window or filter produced a trigger that matched nothing (or everything) while reporting itself as configured.

实测 validateFlowTriggerReadiness 转发后的 finding 全长(#5496 那个真实描述符,三个 zod issue):

用例 message 长度
#5496 原始描述符(field + 缺 dateField + 标量 offsetDays) 709
单个拼错键(offsetDay) 595
命中 guidance 的键(schedule) 779
无未知键(只违反「恰好其一」) 316

对比第 1 行与第 4 行:history 句是这条 finding 近 1/3 的长度,且它的位置在「field 错了」与「Did you mean field → dateField?」之间。

为什么现在才碍事

CLI 把 finding 打成单行(packages/cli/src/commands/validate.ts:141):

console.log(`  • ${f.where}: ${f.message}`);

在 warning 块里,读者可以整段跳过。#5762 之后这条规则进入 error 块,作者必须据此动手 —— 而修法(Did you mean …)落在 709 字符的最末。history 讲的是「#4001 之前会怎样静默失败」,对正在修的这个人没有动作价值。

history 的设计初衷是好的:让「以前会静默剥离」这件事可追溯。问题只在渲染顺序与消费位置 —— 它被拼在消息中段,而消费方之一是单行 error 显示位。

为什么没在 #5762 内修

四条路都不干净,PR #5952 正文已记录:

  1. 消费者侧正则剥离 —— 要在 packages/lint 里重述生产者的消息结构(Unrecognized key(s) on …: …. + history + Did you mean …?),正是 validate-flow-trigger-readiness.ts 模块注释声明要避免的「契约的第二份拷贝」,也违反 contract-first(消费者容忍换不来正确性)。
  2. 按长度截断 —— Did you mean 在末尾,截断恰好切掉最有用的部分,比不截更糟。
  3. 消费者查生产者拿 history 原文 —— 拿不到:strictObjectDeclarations()(shared/strict-object.ts)与 directAliasTables()(shared/alias-table-registry.ts)都刻意不进 barrel,是内部审计接缝,且前者无 schema 身份可匹配。
  4. 生产者侧改渲染 —— 干净,但影响面是 spec 的授权错误契约对外变更:全仓 62 个 strictObject 站点 / 293 处 history: 声明,以及跨包钉死这些文案的测试。属独立 PR + 独立拍板。

候选方向(供分诊/拍板)

  • A. history 排到消息末尾(… keys. Did you mean …? <history>)。最小改动、修法前置,信息一句不丢。代价:所有钉死完整消息顺序的测试要跟着改。
  • B. history 只在 guidance/rename 都无话可说时才拼。噪音只在「有明确修法」时消失,但规则变得依赖分支、可解释性下降。
  • C. 结构化返回,由消费方决定渲染(单行位丢 history,完整报告位保留)。最正确也最贵:zod 的 error map 只能返回字符串,要另开通道。
  • D. 不改,承认单行 error 位不适合承载沿革,由消费者改用多行渲染(CLI 把 message 折行/分段)。改的是 packages/cli 而非 spec,影响所有 finding。

倾向 A —— 它不删任何信息(#5762 裁定的「保留原始信息可达路径」自动满足),只把「对正在修的人有动作价值的部分」排到前面,且是四条里唯一既不新增分支也不新增通道的。

关联:#5762(实测出处)、PR #5952(记录了完整推理)、#4001(history 机制的来源)、#5593(strictUnknownKeyError 直调点迁移 —— 不同关切:机制迁移,非消息版式)。

Activity

  1. claude commented on Aug 6, 2026

    @claude
    Contributor

    分诊:needs-user-decision(公共授权错误契约的版式取舍)+ 改域 domain:devx → domain:spec。

    ⚠️ 本单此前处于**「半分诊」态**(有 tooling + domain:devx,无 pm-state 标签)⇒ 对队列视图、决策箱、finding 池三者皆不可见。本轮补状态标签并订正域。

    为什么改域(anchoring rule,⛔ 按落点不按标题词汇)

    原 domain:devx 大概率是按「lint / CLI 显示位」这个症状面打的。但四条候选路线的落点是:

    路线 落点包 域
    A(history 排到消息末尾,立单人倾向) packages/spec/src/shared/suggestions.zod.ts:321 spec
    B(仅在无 guidance/rename 时才拼 history) 同上 spec
    C(结构化返回,消费方渲染) spec 生产者 + 各消费方 spec 主导
    D(不改生产者,CLI 改多行渲染) packages/cli/src/commands/validate.ts:141 cli

    四条里三条(含倾向路线 A)主改动面在 packages/spec ⇒ 按 anchoring rule 归 domain:spec;且**跨座位转移协议的「shared contract surfaces have one owner」**要求:凡触 packages/spec 一律归 domain:spec 座位,无论谁需要它。⇒ 改标。旁证:同文件的 #5593(strictUnknownKeyError 直调点迁移)已带 domain:spec。

    为什么进决策箱而不是入队

    history 拼接位置的改动是对外授权错误契约的措辞/版式变更,不是内部实现:

    • 影响面立单人已量化 —— 全仓 62 个 strictObject 站点 / 293 处 history: 声明,外加跨包钉死完整消息顺序的测试;
    • 四条路线各有实测记录的代价(消费者侧正则剥离=契约的第二份拷贝、按长度截断=恰好切掉 Did you mean、查生产者拿原文=接缝刻意不进 barrel 且无 schema 身份可匹配);
    • 立单人自陈路线 A「属独立 PR + 独立拍板」。

    ⇒ 「作者看到的错误消息里,沿革句排在修法之前还是之后」是产品措辞取舍,PM 不代拍。决策卡已就绪:四条路线、各自代价、倾向与判据均在正文,维护者可直接选。

    过时前提检查(origin/main 9e3709a)—— 成立:suggestions.zod.ts:321 的拼接式 `Unrecognized key(s) on ${surface}: ${keys…}. ${history}` 原样在;:244-245 的 history: string 声明在;:291 的解构在。⇒ 版式事实面未变。

    在飞面:三仓 open PR 逐条拉过,无 PR 触 packages/spec/src/shared/suggestions.zod.ts ⇒ 裁决落地后无文件冲突。

    查重(与 #5593 明确不收敛):#5593 是机制迁移(44 个直调点搬到 strictObject,棘轮降 0),本单是消息版式;两单同文件但判据、验收、争点均不同,正文已互链。⛔ 不合并 —— 但若两者先后落地,#5593 在先(它已 pm:queue + pm:blocked)可减少本单要改的站点数,届时请重新定价。三仓无其它影子单。

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


    Generated by Claude Code

  2. claude commented on Aug 7, 2026

    @claude
    Contributor

    Disposition (2026-08-07): returned to the queue — reordering a rendered message is not a contract question.

    Direction A: move the surface-history sentence to the end of the message, so "which key is wrong" is immediately followed by "did you mean". No text is deleted, nothing becomes unreachable, and the change is one concatenation point. Since #5762 promoted these to error level, the fix currently lands past character 222 on a single-line display — the author (often an AI) reads the front of the message and acts on it.

    Options B (conditional history) and C (structured returns) both add machinery for a smaller gain — zod's error map hands back a string either way; D (multi-line CLI rendering) doesn't help the other single-line consumers such as CI logs.

    Scheduling: #5593 touches the same surface and both pin the full message text in tests. Whichever lands first, re-price the other before dispatching.

    Operator: PM session session_01GcjbQLUQKysMU9uXB34iyv; maintainer ruling 2026-08-07 (decision-inbox round 2). Veto window open — comment or reopen to overturn.


    Generated by Claude Code

  3. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    Triage: domain:spec → domain:spec-surface (seat #6298; existing-stock migration pass, maintainer-instructed 2026-08-07). Squarely in the seat's declared face ("错误 guidance"): the 2026-08-07 ruling (direction A, history sentence moved to the end) is one concatenation point and was explicitly ruled "not a contract question" — what gets rejected and why is unchanged, only the order the sentence is read in. The tooling topic label is kept; it is not a domain.


    Generated by Claude Code

  4. self-assigned this
    on Aug 7, 2026
  5. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    Claim: PM loop round 2 (domain:spec-surface seat #6298)
    Session: session_01JTSZAjgtL3oR6YcpNDhW3T
    Branch: claude/issue-5955-history-sentence-to-end
    Worktree: objectstack-issue-5955
    Domain: domain:spec-surface
    File surface: packages/spec/src/shared/suggestions.zod.ts (the :321 concatenation point), plus test files repo-wide that pin the FULL strictUnknownKeyError message order (mechanical reorder-pin updates only), .changeset/*.md (stop on breach; explain in the report)
    Serial constraints cleared: same-file sibling #5593 is pm:blocked, not in flight — per the maintainer's 2026-08-07 ruling on this issue, whichever of the two lands second gets re-priced (this lands first; the re-pricing answer for #5593 is a required item in this dispatch's report). Triage verified 2026-08-06: no open PR touches suggestions.zod.ts. Within this round's batch, this PR lands last (its pin-reorder sweep absorbs any full-message assertions the sibling PRs #5767/#5622 may add).
    Container weight: M (repo-wide pinned-test radius), mode:subagent shared container.


    Generated by Claude Code

  6. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    Review: ACCEPT on substance — PR #6375 (landing gated on CI convergence)

    Reviewed by the domain:spec-surface seat (#6298, session session_01JTSZAjgtL3oR6YcpNDhW3T) against the diff.

    Ruling executed as written. Direction A, one concatenation point, history emitted last. No deletion, no conditional (rejected B), no structural change (rejected C), no CLI change (rejected D). 4 files: suggestions.zod.ts, the new pin block in strict-object.test.ts, and one changeset correction.

    The dev falsified the PM's mechanism assumption, and it changed the implementation

    My dispatch asked whether the \n • segments are a per-key multi-error joiner — in which case history would belong at the end of each segment. Measured answer: they are not. zod raises one unrecognized_keys issue per rejected object naming every offending key, so the surface's history appears exactly once per message regardless of key count (now pinned as its own case). The \n • segments are the guidance prescriptions, joined inside that single message.

    That finding is not trivia — it changed what "correct" means here. There are two fix channels, not one, and both had to move ahead of the sentence, because a guidance prescription is as actionable as a rename. Putting history after the renames but before the bullets would have satisfied a naive reading of the ruling while re-creating the exact defect for guidance-hit keys — which is row 3 of this issue's own measurement table (schedule, 779 chars). A dev that took the dispatch at face value would have shipped a fix that misses the worst-measured case in the card that commissioned it.

    Byte-identical message lengths in all four measured cases is the right evidence that this is a reorder and not an edit, and Did you mean moved from 443→219 and 329→105. The dev also corrected this issue's own figure: the specimen history sentence is 224 chars, not 222 (one em-dash and a space) — structural claim unchanged.

    The required report item: #5593 re-pricing is zero

    Pinned-message test sites touched: 0 — and this is a measured finding, not a sweep gap, which is the distinction that makes it usable. The dev falsified the "my scanner is broken" explanation rather than leaving a bare zero: every existing assertion on this message is a single-fragment toContain/toMatch; the five ordered regexes in packages/lint/src/validate-expressions.test.ts pin front-matter before suggestion — a relation this change preserves, so they were already tolerant in the direction that moved (verified green, not edited); etl.test.ts:432 pins presence, not position.

    So the ruling's scheduling caveat — "#5593 touches the same surface and both pin the full message text in tests" — is priced at zero from this side. #5593 needs no re-pricing on account of this PR and is neither easier nor harder.

    And the sweep found the one artifact that did depict the old order: the pending changeset .changeset/action-param-strict-unknown-keys.md (from #3405, on main — I verified it is not another in-flight PR's file), whose worked example showed Did you mean behind the history sentence. Flipped verbatim against real parser output. The sibling .changeset/format-zod-error-union-branches.md was checked and deliberately not touched, because its example (args on ActionRefSchema) has no alias and no guidance and is therefore unchanged. Catching a stale worked example that would otherwise ship into release notes is exactly what a sweep is for.

    The new pin is load-bearing. My dispatch demanded that flipped pins keep bearing load; the honest outcome was that there were none to flip and no order pin existed at all, so the dev created one — five cases, each asserting all three facts (front matter first, fix before history, history still present verbatim at the end). Reverse verification predicted plain red and measured 3 failed / 20 passed, with the two green ones correctly explained as order-independent by construction rather than glossed over.

    Landing

    ⛔ Not flipped to ready — ESLint and TypeScript Type Check are both in_progress. Flip follows once both conclude success.

    Landing-order constraint dissolved. I had scheduled this PR to land last so its pin sweep could absorb full-message assertions the sibling PRs might add. That rationale no longer holds, on measurement: #6368 (#5622) asserts only validateActionParams' own inline message template, which does not route through strictUnknownKeyError, and #6364 (#5767) adds no message-order assertions at all. Zero interaction across the batch, so this PR may land in whatever order CI clears it. #6368 is already queued ahead of it, which is fine but no longer required.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions