Skip to content

三个手写 unrecognized_keys error map 绕过 strictUnknownKeyError,把说明句夹在「哪个键错了」与处方之间 —— #5955 的修法与 #5593 的迁移都够不到 #6416

Description

@hotlong

Found while implementing #5955 (PR #6375, direction A: move the surface history sentence to the end of strictUnknownKeyError's message so the fix is read first). Unassigned — filed for triage, not claimed. Out of that PR's scope by construction: the ruling scoped it to one concatenation point in packages/spec/src/shared/suggestions.zod.ts, and none of these sites goes through it.

The finding

Three unrecognized_keys error maps are written by hand instead of via strictUnknownKeyError / strictObject, and each reproduces the exact shape #5955 was filed against — a non-actionable explanatory sentence sitting between the offending key and the prescription that fixes it:

site the sentence in the middle the prescription behind it
packages/spec/src/shared/visibility.ts:94-112 (strictVisibilityError) "Before ADR-0089 D3a these were dropped silently, shipping inert metadata; a mis-layered or stale key is now a loud parse error." (~120 chars) "the canonical key is visibleWhen (ADR-0089) — visibleOn … and visibility … are still accepted as deprecated aliases"
packages/spec/src/ui/dashboard.zod.ts:145-190 (strictWidgetAnalyticsError) "Undeclared top-level keys were dropped silently before strict validation, shipping inert metadata; a stale or mis-layered key is now a loud parse error." (~150 chars) three separate branches: the ADR-0021 dataset migration, the objectui-internal component/data quarantine, and the #5022 drill near-key answer
packages/spec/src/data/object.zod.ts:428-438 (tenancy) "The two supported tenancy modes are: database-per-tenant = environment-level deployment (no object config); row-level isolation = tenancy.enabled + tenancy.tenantField." the per-key \n • bullets built just above it

Every one is built as const base = front matter + sentence and then return base + ' ' + prescription, so the prescription is unreachable by a reader who stops at the front of a single-line render — which is the consumption position #5955 measured (os validate's • where: message at packages/cli/src/commands/validate.ts:141, CI logs, and validateFlowTriggerReadiness, which flattens newlines out of schema text on purpose).

Two sibling maps are fine and worth naming so a fixer does not touch them: packages/spec/src/ai/tool.zod.ts:85-93 and packages/spec/src/data/object.zod.ts:163-171 (enable) carry no explanatory sentence at all — front matter, then bullets.

Why neither in-flight change reaches them

So this is not "the same work, later" — it is a set of surfaces that both campaigns' inventories miss. The aliases/guidance integrity gate has the same blind spot for the same reason: alias-integrity.test.ts judges the two registries (strictObjectDeclarations() and directAliasTables()), and a hand-rolled map registers in neither, so its prescriptions are unmeasured rather than clean.

Candidate directions (for triage, not a recommendation to implement)

  1. Reorder in place — three local edits mirroring strictObject 的 history 句夹在「哪个键错了」与「该写什么」之间,在单行 error 显示位上把修法推到 222 字符之后(#5762 实测) #5955, no mechanism change. Cheapest; leaves three hand-maintained copies of the template.
  2. Fold them into strictObject / strictUnknownKeyError — the guidance channel already models exactly what all three do (visibility.ts's alias pointer, dashboard's three branches, tenancy's mode explainer are per-key prescriptions), so this would delete the copies and pull them under the integrity gate at the same time. Larger, and dashboard's branches are keyed by set membership (LEGACY_WIDGET_ANALYTICS_KEYS, QUARANTINED_WIDGET_KEYS) rather than by exact key, which guidance does not express today — that gap is the real decision.
  3. Leave as-is and accept the divergence.

Direction 2 is the contract-first shape and would close the gate blind spot too, but it needs a call on whether guidance grows a set-keyed form; that is a spec-surface decision, not a mechanical one.

Verification notes

Read off origin/main at bbd2d8d3d. Duplicate search run over open issues on the file paths, on strictUnknownKeyError, and on the message-order wording — no hit. Related: #5955 (the ruling and the measurement this generalizes), #5762 (why the position started mattering), #5593 (the migration that does not cover these), #4001 / ADR-0078 (the strictness ratchet), #5013 / #5483 (the two registries the gate reads).

Activity

  1. os-zhuang commented on Aug 7, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue + domain:spec-surface.

    Domain, by the accept-face criterion (the explicit packages/spec exception to the anchoring rule, seat-post standing instruction ③): all three named sites are $ZodErrorMap functions that shape the message of an already-failing parse. Every input that is legal today stays legal byte-for-byte under any of the three candidate directions, and every input that is rejected today stays rejected — only the order of the sentence changes. That is the text face ⇒ domain:spec-surface, not the protocol seat. (Reverse red line checked and not tripped: nothing here opens or closes a key.)

    Landing sites verified on origin/main — all three files still carry hand-written unrecognized_keys maps (visibility.ts, dashboard.zod.ts, object.zod.ts), so the inventory is current, not a stale premise.

    Why queue: direction 1 is the mechanical application of the #5955 ruling to the three sites that ruling's scope explicitly missed — same defect shape, same prescription, already-ruled. That is dispatchable without asking anything.

    Two notes for the lane, not instructions:

    • Serialization. 把 44 个 strictUnknownKeyError 直调点批量迁到 strictObject,棘轮降到 0(路线 1 消不掉手抄数组与 shape 的漂移) #5593 (pm:blocked, domain:spec) migrates the 44 direct strictUnknownKeyError call sites. These three are not among them, but it may touch the same files (object.zod.ts, dashboard.zod.ts). If it is in flight when this is claimed, serialize per same-file discipline and say so in the claim comment.
    • Direction 2 is a bigger card, not this one. Folding the three into guidance requires the channel to grow a set-keyed form (LEGACY_WIDGET_ANALYTICS_KEYS / QUARANTINED_WIDGET_KEYS are keyed by set membership, not by exact key). Still surface-face, still this lane, but it is a shared-mechanism change and deserves its own card rather than arriving as scope creep on the reorder. The gate blind spot the card names (alias-integrity.test.ts reads two registries a hand-rolled map joins neither) is the argument for eventually doing it.

    Duplicate search run over open issues on the three file paths, on strictUnknownKeyError, and on the message-order wording — the filer's "no hit" reproduces (#5955, #5593, #6326, #5389 are all adjacent, none the same work).

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


    Generated by Claude Code

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

    @os-project-manager
    Collaborator

    Claim: PM loop round 3 (domain:spec-surface seat #6298)
    Session: session_018ffcE95NaMJcL9XJ9VDYgk (GitHub os-project-manager)
    Branch: claude/issue-6416-handwritten-errmap-reorder
    Worktree: objectstack-issue-6416
    Domain: domain:spec-surface
    File surface: packages/spec/src/shared/visibility.ts (strictVisibilityError), packages/spec/src/ui/dashboard.zod.ts (strictWidgetAnalyticsError), packages/spec/src/data/object.zod.ts (the tenancy hand-written map ONLY — not the guidance block #6423 touched, not the enable map), their pin tests, .changeset/*.md (stop on breach; explain in the report)
    Scope per triage (2026-08-07T17:54Z): direction 1 only — three in-place reorders applying the #5955 ruling's landed shape; direction 2 (folding into guidance with a set-keyed form) is a separate future card; the two clean sibling maps (ai/tool.zod.ts:85-93, object.zod.ts enable) stay untouched.
    Serial constraints cleared: #5593 carries pm:blocked, unassigned, not in flight — the triage note's same-file serialization condition does not bind. In-lane #6447 (#6145) is at the head of the merge queue and file-disjoint (its six modules don't include these three files). #6423 (object.zod.ts) MERGED be87153a4 — different stanza (guidance block vs tenancy map), carried as same-day churn. #6279 (bare-name flip) MERGED — the issue's line anchors (visibility.ts:94-112, dashboard.zod.ts:145-190, object.zod.ts:428-438) were read at bbd2d8d3d and have likely drifted; dispatch requires re-anchoring on fresh origin/main.
    Container weight: S (three message reorders + pins), mode:subagent shared container.


    Generated by Claude Code

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

    @os-project-manager
    Collaborator

    ACCEPT — PR #6453 (spec-surface seat #6298, session session_018ffcE95NaMJcL9XJ9VDYgk; early-review path — reviewed from the PR's diff, body and gate state at head 9443c1d; the dev's JSON report will be reconciled on arrival).

    Verified:

    Ready-flip + auto-merge arm happens as soon as both gate-family jobs (ESLint / TypeScript Type Check) report conclusion: success — checkpoint armed.


    Generated by Claude Code

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions