Skip to content

fix(spec): move the strictObject history sentence to the end of the message (#5955) - #6375

Queued
hotlong wants to merge 1 commit into
mainfrom
claude/issue-5955-history-sentence-to-end
Queued

fix(spec): move the strictObject history sentence to the end of the message (#5955)#6375
hotlong wants to merge 1 commit into
mainfrom
claude/issue-5955-history-sentence-to-end

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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:

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.

The change

strictUnknownKeyError assembled its message as front matter → history → fix.
The history sentence is now emitted last, after both fix channels.

-    let message =
-      `Unrecognized key(s) on ${surface}: ${keys…}. ${history}`;
+    let message = `Unrecognized key(s) on ${surface}: ${keys…}.`;
     if (renames.length) message += ` Did you mean ${renames.join(', ')}?`;
     if (prescriptions.length) message += `\n${prescriptions…}`;
-    return message;
+    return `${message} ${history}`;

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-error
joiner, in which case history would belong at the end of each segment. It is
not.
Measured on origin/main:

  • zod raises one unrecognized_keys issue per rejected object, naming
    every offending key in it, so the surface's history appears exactly once
    per message
    regardless of key count. That is pinned as its own test.
  • The \n • segments are the guidance prescriptions — tombstones and
    wrong-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 guidance prescription is as actionable as a rename. Putting
history 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:

Unrecognized key(s) on {surface}: `k1`, `k2`.   which keys are wrong
[ Did you mean `k1` -> `canonical`? ]           fix, channel 1 (renames)
[ newline + "  • " + {guidance} ]               fix, channel 2 (prescriptions)
{history}                                       why it used to be silent

Measurement, before and after

TimeRelativeTriggerSchema (the #5762 / #5496 specimen). Its history sentence
measures 224 characters, not 222 — the issue's count was one em-dash and a
space out; the structural claim is unchanged.

case len before len after Did you mean at
#5496 descriptor (field + missing dateField + scalar offsetDays) 480 480 443 -> 219
single misspelled key (offsetDay) 366 366 329 -> 105
guidance hit (schedule) 544 544 bullet at 92 (was 316)
guidance + rename in one message 598 598 105 (was 329)

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), across
packages/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:

  • Every existing assertion on this message is a toContain / toMatch on a
    single fragment. All of them stayed green with the sentence in the middle
    and stay green with it at the end.
  • The five ordered regexes in 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:432 asserts toContain('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 worked
example is a full message with Did you mean behind the history sentence. It
is flipped here, verbatim, no text dropped — verified against the real parser
output. Its sibling .changeset/format-zod-error-union-branches.md was checked
and deliberately not touched: its example is args on ActionRefSchema,
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.ts gains a
message 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 toBe for 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:

FAIL  message order … > names the wrong key first, then the rename, then the history
  Expected: "`colummSpan`. Did you mean `colummSpan` → `columnSpan`?"
  Received: "Unrecognized key(s) on this widget: `colummSpan`. Until #4001 these were
             dropped silently — the widget still rendered. Did you mean `colummSpan` → `columnSpan`?"
FAIL  message order … > puts a guidance prescription ahead of the history too
FAIL  message order … > keeps BOTH fix channels ahead of the history in one message
Tests  3 failed | 20 passed (23)

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

pnpm --filter @objectstack/spec test        Test Files 332 passed (332)  Tests 8496 passed (8496)
pnpm --filter @objectstack/lint test        Test Files  61 passed (61)   Tests 1504 passed (1504)
pnpm --filter @objectstack/cli
       --filter @objectstack/metadata-protocol test        exit 0, all green
pnpm --filter @objectstack/spec exec tsc --noEmit          PASS

Every check:* step enumerated from .github/workflows/lint.yml, run one by
one — all PASS. Two are worth naming because the dispatch predicted them:
check:authorable-surface (zero key drift, anchor untouched) and
check:docs (no content/docs/references/** regen — an error message is not
describe text). check:nul-bytes clean, plus a manual control-byte self-scan
over every touched file.

Effect on #5593

strictObject already delegates to strictUnknownKeyError, so the reorder
lands 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

`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
@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 3:28pm

Request Review

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

112 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/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/spec)
  • 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/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/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/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @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/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @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/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/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.

hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

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 (GH006 … Branches that are queued for merging cannot be updated). Not dequeuing: the queue rebuilds this PR as merged onto current main and re-runs the subscribing workflows on that generation, which is exactly the re-verification the lap exists to do — done race-free by the platform instead of by me.

What the lap found, so nothing is lost:

Merge. origin/main had advanced 35 commits (db59e9c5fbbd2d8d3d). git merge origin/main merged clean, no conflicts, no textual overlap with either sibling PR.

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 toContain, i.e. presence assertions that are order-agnostic in both directions:

  • packages/lint/src/authoring-rule-input-tier.test.ts:131,192toContain('Unrecognized key(s) on this view container') / '… on this list view'): front matter only, never reaching the moved sentence.
  • packages/lint/src/validate-component-props.test.ts:53,136 and packages/spec/src/ui/action-params.test.ts:143,158,214Did you mean \title`?/Did you mean the built-in "recordId"?. Different producers entirely (the component-props rule and the action-param rule write their own hints); strictUnknownKeyErrorrenders a rename as ``kcanonical` ``, which none of these match.

The only boundary-spanning strings left anywhere in the tree are the two already accounted for in the PR body — the flipped .changeset/action-param-strict-unknown-keys.md, and .changeset/format-zod-error-union-branches.md, whose args-on-ActionRefSchema example has no alias and no guidance and is therefore byte-identical under both orders — plus packages/lint/src/validate-org-axis-red-lines.ts:74, where the "Until #5009" is the comment's own prose and not part of the message, and packages/spec/CHANGELOG.md (released history, never edited).

Two new gates arrived with the merge (check:quick-reference-counts, check:meta-type-normalized) — both run against the merged tree: PASS.

Re-verification on the merged tree.

pnpm --filter @objectstack/spec test   Test Files 337 passed (337)  Tests 8609 passed (8609)
pnpm --filter @objectstack/lint test   Test Files  62 passed (62)   Tests 1519 passed (1519)

One thing worth recording because it read as a real red for a lap: @objectstack/lint first came back 4 failed | 1515 passed, all four in validate-component-props.test.ts (dataSource supplying object, the retired displayField prescription, count → icon). None of it touches message ordering — it was the §9 stale-artefact trap in mirror image: the merge advanced packages/spec's source by 35 commits while packages/spec/dist still held the pre-merge build that @objectstack/lint resolves through, so the merged tests were being judged against the old spec. pnpm --filter '@objectstack/lint^...' build and the same command is green at 62/62. Recording it here so the next reader of this branch does not re-diagnose it as a regression from the reorder.


Generated by Claude Code

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/m tests tooling

Projects

None yet

2 participants