fix(spec): move the strictObject history sentence to the end of the message (#5955) - #6375
Conversation
`strictUnknownKeyError` 的消息此前拼成「前置语 → history → 修法」, 每个 surface 自己声明的沿革句(「这个键以前会被静默丢弃」)正好夹在 作者真正需要的两件事之间:哪个键错了,和该写什么。 #5762 把 `flow-time-relative-descriptor-invalid` 升为 error 之后这一点 开始碍事:多个消费方把 finding 打成单行 —— `os validate` 的 `• where: message`、CI 日志,以及 `validateFlowTriggerReadiness` (它刻意把 schema 原文里的换行压平,好让 CLI 的项目符号列表保持对齐)。 `TimeRelativeTriggerSchema` 的 history 句是 224 字符,于是在 #5496 的 描述符上,`Did you mean` 落在 480 字符行的第 443 位,前面挡着一句没有 动作价值的沿革。作者(常常是 AI)只读这行的开头就动手了。 现在沿革句排到最后,两个修法通道(rename 与 guidance 处方)都排在它 前面 —— 一条处方和一次改名一样可执行,不能留在句子后面。 一处拼接点改动,消息长度逐字节不变(480/366/544/598),没有删任何文本、 没有加任何条件分支。新增顺序钉测试 5 条置于 `strict-object.test.ts`; 反向验证:把旧拼接式放回去,其中 3 条转红。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTSZAjgtL3oR6YcpNDhW3T
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
Merge-main lap (the dispatch's landing-order step), run locally and reported here because the branch could not be updated — this PR had already been marked ready and added to the merge queue, and a queued branch is push-protected ( What the lap found, so nothing is lost: Merge. Pin re-sweep against the merged tree — zero additional flips. The three test files the merged commits added or changed that touch this message are all single-fragment
The only boundary-spanning strings left anywhere in the tree are the two already accounted for in the PR body — the flipped Two new gates arrived with the merge ( Re-verification on the merged tree. One thing worth recording because it read as a real red for a lap: Generated by Claude Code |
…i#6416) (objectstack-ai#6453) `strictVisibilityError` (shared/visibility.ts)、`strictWidgetAnalyticsError` (ui/dashboard.zod.ts)、`strictTenancyError` (data/object.zod.ts) 都是独立的 `$ZodErrorMap`,既不经过 `strictUnknownKeyError`(所以 objectstack-ai#5955 的修法够不到), 也不在 objectstack-ai#5593 迁移的 44 个直接调用点里。三者都复现了 objectstack-ai#5955 针对的形状:一句 不可操作的说明句夹在「哪个键错了」与修复它的处方之间 —— 在多个消费方的单行 渲染上(`os validate` 的 `• where: message`、CI 日志、会主动抹平换行的 `validateFlowTriggerReadiness`),处方被推出作者真正会读的那一段。 现在统一为 PR objectstack-ai#6375 为共享模板落地的顺序:前置语(哪个键错了)→ 全部修复 通道(`visibleWhen` 别名指针;ADR-0021 dataset / objectui 隔离 / objectstack-ai#5022 drill 三个分支;`tenancy` 的逐键 tombstone bullets)→ 说明句放最后。没有删除任何 文本,也没有让任何句子变成有条件的:七个消息变体在长度与字符多重集上与原拼接 逐字节相同。接受面不动 —— 这些 map 只塑造一个已经失败的 parse 的文本, `visibility.ts` 的别名表原样未改。 Claude-Session: https://claude.ai/code/session_018ffcE95NaMJcL9XJ9VDYgk Co-authored-by: Claude <noreply@anthropic.com>
Fixes #5955
Direction A of the 2026-08-07 ruling, executed as one concatenation point in
packages/spec/src/shared/suggestions.zod.ts. Quoted verbatim:The change
strictUnknownKeyErrorassembled its message as front matter → history → fix.The
historysentence is now emitted last, after both fix channels.Nothing else changed: no deletion, no conditional (rejected option B), no
structural/API change (rejected option C), no CLI change (rejected option D).
The actual template shape (the PM's mechanism assumption, measured)
The dispatch asked whether the
\n •segments are a per-key multi-errorjoiner, in which case history would belong at the end of each segment. It is
not. Measured on
origin/main:unrecognized_keysissue per rejected object, namingevery offending key in it, so the surface's
historyappears exactly onceper message regardless of key count. That is pinned as its own test.
\n •segments are theguidanceprescriptions — tombstones andwrong-layer pointers — joined inside that single message, not per-key error
segments.
So there are two fix channels, not one, and both had to move ahead of the
sentence: a
guidanceprescription is as actionable as a rename. Puttinghistory after the renames but before the bullets would have re-created the exact
defect for guidance-hit keys — which is measured row 3 of the issue's own table
(
schedule, 779 chars). Final order:Measurement, before and after
TimeRelativeTriggerSchema(the #5762 / #5496 specimen). Its history sentencemeasures 224 characters, not 222 — the issue's count was one em-dash and a
space out; the structural claim is unchanged.
Did you meanatfield+ missingdateField+ scalaroffsetDays)offsetDay)schedule)Message lengths are byte-identical in every case — the strongest available
evidence that this is a reorder and not an edit.
Pin sweep — the number #5593 needs
Repo-wide sweep for assertions that pin the full message in order
(
Unrecognized key(s) on,Did you mean, and every history phrasing:Until #,dropped silently,previously,before ADR), acrosspackages/spec,packages/lint,packages/cli,packages/runtime,packages/rest,packages/objectql,packages/metadata-protocol,packages/services/*,packages/plugins/*:Pinned-message test sites touched: 0. Not a gap in the sweep — a measured
finding, and the reason a new pin was required:
toContain/toMatchon asingle fragment. All of them stayed green with the sentence in the middle
and stay green with it at the end.
packages/lint/src/validate-expressions.test.ts(
/Unrecognized key\(s\) on this object: \validationRules`.*Did you mean …/s`and four siblings) pin front-matter before suggestion — the relation this
change preserves — so they were already order-tolerant in the direction
that moved. Verified green, not edited.
packages/spec/src/automation/etl.test.ts:432assertstoContain('Until #4001'): presence, not position. Green.The one artifact in the repo that did depict the old order is a pending
changeset,
.changeset/action-param-strict-unknown-keys.md, whose workedexample is a full message with
Did you meanbehind the history sentence. Itis flipped here, verbatim, no text dropped — verified against the real parser
output. Its sibling
.changeset/format-zod-error-union-branches.mdwas checkedand deliberately not touched: its example is
argsonActionRefSchema,which has no alias and no guidance, so that message is unchanged by this PR.
The new pin
packages/spec/src/shared/strict-object.test.tsgains amessage order — the fix comes before the history (#5955)block, five cases:rename-before-history, prescription-before-history, both-channels-in-one-message,
history-emitted-exactly-once, and a full-message
toBefor the no-fix case(where the order is genuinely unchanged). Each order case asserts all three
required facts — front matter first, fix before history, history still present
verbatim at the end.
Reverse verification. Direction predicted before running: plain red —
this is a straightforward reorder with a canonical-first chain nowhere in play.
Restoring the old concatenation on top of the new tests:
The two that stay green are honest and intended: "emits the history exactly
once" is an order-independent property, and the no-fix full-message pin is
byte-identical under both orders because there is nothing to put in front.
Verification
Every
check:*step enumerated from.github/workflows/lint.yml, run one byone — all PASS. Two are worth naming because the dispatch predicted them:
check:authorable-surface(zero key drift, anchor untouched) andcheck:docs(nocontent/docs/references/**regen — an error message is notdescribe text).
check:nul-bytesclean, plus a manual control-byte self-scanover every touched file.
Effect on #5593
strictObjectalready delegates tostrictUnknownKeyError, so the reorderlands at the single point both wirings share, and the new pin exercises it
through
strictObject. #5593 is unaffected — neither easier nor harder.Details in the issue report; the number that matters for re-pricing is the one
above: zero pinned-message test sites needed flipping, so the "both pin the
full message text in tests" scheduling concern in the ruling turns out to be
priced at zero from this side.
Generated by Claude Code