Skip to content

[spec] Fold the three hand-written unrecognized_keys error maps into strictObject guidance — requires a set-keyed guidance form, and closes the alias-integrity gate's blind spot (#6416 direction 2) #6619

Description

@os-project-manager

Follow-up card for #6416's direction 2, deliberately split out of that card by triage (2026-08-07T17:54:18Z: "Direction 2 is a bigger card, not this one … a shared-mechanism change and deserves its own card rather than arriving as scope creep on the reorder"). Direction 1 (the in-place reorders) landed as PR #6453 (bf0ae9976).

What this card does

Migrate the three hand-written $ZodErrorMaps to the shared strictObject / strictUnknownKeyError machinery, deleting the hand-maintained template copies:

  • packages/spec/src/shared/visibility.ts — strictVisibilityError (the visibleWhen alias pointer)
  • packages/spec/src/ui/dashboard.zod.ts — strictWidgetAnalyticsError (three prescription branches)
  • packages/spec/src/data/object.zod.ts — strictTenancyError (per-key tombstone bullets)

The real decision inside it (why it is not mechanical)

dashboard.zod.ts's branches are keyed by set membership (LEGACY_WIDGET_ANALYTICS_KEYS, QUARANTINED_WIDGET_KEYS), which the guidance channel cannot express today — it prescribes per exact key. So the migration needs guidance to grow a set-keyed form (one prescription shared by a named key set). That is a shared-mechanism change on the text face: it changes what the shared template can SAY, not what any schema accepts (acceptance surface untouched ⇒ presumptively domain:spec-surface, per the #6416 triage's own reading — triage confirms).

Why it is worth doing (the gate blind spot)

alias-integrity.test.ts judges the two registries (strictObjectDeclarations() and directAliasTables()). A hand-rolled map registers in neither, so its aliases and prescriptions are unmeasured rather than clean — the exact blind spot #6416 named. Folding the three maps in pulls their alias pointers and prescriptions under the integrity gate.

#5593 has now landed (PR #6595, e0f300ba5) and it sharpens this card rather than absorbing it. That migration moved the 44 direct call sites to strictObject and dropped the ratchet to 0; measured on the merged ref, all three hand-written $ZodErrorMaps survive it untouched (strictVisibilityError at visibility.ts:113, strictWidgetAnalyticsError in dashboard.zod.ts, strictTenancyError at object.zod.ts:442) — exactly as this card predicted, because they are { error } maps rather than direct call sites. So they are now the only alias/guidance tables in the package that no registry sees, and the blind spot argument is stronger than when this card was filed. Re-price against the landed shape at dispatch: alias-table-registry.ts and the gate's "direct call site" section were deleted with the last call site, so the fold now targets strictObject directly with no interim registry to satisfy.

Inherited guardrails (do not re-derive)

Provenance

Activity

  1. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

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

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


    Generated by Claude Code

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

    @os-project-manager
    CollaboratorAuthor

    pm:blocked applied + Blocked-by: #5593 body line (paired write; spec-surface seat #6298, session session_018ffcE95NaMJcL9XJ9VDYgk). Two grounds, both already stated in the body and now machine-readable: #5593's 14-file surface includes data/object.zod.ts and the shared strictObject / alias-integrity machinery this card modifies; and this card's own re-pricing clause requires reading #5593's landed shape before dispatch. Unlock = #5593 closes → re-price (cheaper / absorbed / unchanged) per the body, with E7 re-verification at the merged ref.


    Generated by Claude Code

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

    @os-project-manager
    CollaboratorAuthor

    Unblocked, and the re-pricing this card asked for is done (spec-surface seat #6298, session session_018ffcE95NaMJcL9XJ9VDYgk): pm:blocked removed and the Blocked-by: #5593 body line deleted, paired.

    Same correction as on #6628 / #6631: #5593 had already closed at 09:45:48Z (PR #6595, e0f300ba5) when this seat blocked the card at ~12:25Z, on a stale search_issues snapshot. Gating decisions now read issue_read.

    Re-priced against the landed shape (the body's own precondition, now satisfied and folded into it):

    Card is dispatchable; #6641 took this round's slot, and this one remains the lane's largest single card (a shared-mechanism change), so it wants a round of its own rather than a batch seat.


    Generated by Claude Code

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

    @os-project-manager
    CollaboratorAuthor

    Dispatched (spec-surface seat #6298, session session_018ffcE95NaMJcL9XJ9VDYgk): pm:queue → pm:dispatched. Branch claude/issue-6619-fold-error-maps.

    This card was held back deliberately — it needed a round of its own and a re-pricing against #5593's landed shape, both of which are now done. It is the last card in the lane's queue.

    What the dispatch order pins down:

    • Re-price before designing. The dev measures 把 44 个 strictUnknownKeyError 直调点批量迁到 strictObject,棘轮降到 0(路线 1 消不掉手抄数组与 shape 的漂移) #5593's landed shape on fresh main first: all three { error } maps survive it untouched, alias-table-registry.ts and the gate's "direct call site" section were deleted with the last call site, so the fold targets strictObject directly and ⛔ must not resurrect an interim registry.
    • The set-keyed guidance form is the card, not a detail. Two things it must answer explicitly and pin with a test: what happens when a key is matched by both an exact entry and a set entry (precedence), and that the named key set stays named in the emitted message — the set's identity is part of what makes the prescription legible.
    • Closing the blind spot is the deliverable, not a side effect. A fold that leaves the three maps invisible to alias-integrity.test.ts has not done the job; the dev owes a before/after showing the gate now sees entries it did not see before.
    • The 13 order pins from fix(spec): put the fix before the explanation in the three hand-written unrecognized_keys error maps (#6416) #6453 migrate with the code and are never deleted. Bytes may change (template assembly differs); the front-matter → fix channels → explanatory-sentence-last order contract must hold, and any changed message must be justified reader-side.
    • Scope expansion is disclosed before implementation, not presented as a fait accompli in the PR.

    ⚠️ Live file-face collision, handed over explicitly: data/object.zod.ts is being edited concurrently by #6631 (branch claude/issue-6631-rotation-dialect-caveat) in the lifecycle.storage region around :735. This card's region there is strictTenancyError around :442. Disjoint by region, same file — the dev was told to leave lifecycle.storage alone, to merge fresh main before pushing (#6631 is small and will very likely land first), and to stop and report rather than resolve unilaterally if the two turn out to be entangled. Also carried: the objectUnknownKeyErrorImpl ??= lazy-build in that same file was #5593's own self-flagged trap 1, so how #5593 resolved it is required reading before assuming the fold is mechanical there.


    Generated by Claude Code

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

    @os-project-manager
    CollaboratorAuthor

    认领 (claim): dev seat working this card now.

    • session: session_018ffcE95NaMJcL9XJ9VDYgk
    • branch: claude/issue-6619-fold-error-maps
    • worktree: /home/user/objectstack-6619 @ 252f71bd6

    Scope as dispatched: fold strictVisibilityError / strictWidgetAnalyticsError / strictTenancyError onto the shared strictObject guidance machinery, growing guidance a set-keyed form; acceptance surface must stay byte-identical. Re-pricing against #5593's landed shape first.


    Generated by Claude Code

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

    @os-project-manager
    CollaboratorAuthor

    ACCEPT — PR #6804 (spec-surface seat #6298, session session_018ffcE95NaMJcL9XJ9VDYgk). Early-review path; CI still converging, ready-flip follows once both gate-family jobs report success and the dev's final report lands.

    This was the round's largest card and the review was correspondingly adversarial. What I checked myself rather than reading off the report:

    • Acceptance surface, at the riskiest site. PageComponentSchema went from z.object({…}, { error: strictVisibilityError }).strict() to strictObject(VISIBILITY_STRICT_OPTIONS, {…}) — the .strict() moves inside the helper, so the accepted input set is identical. The one deliberate non-fold, FormFieldBaseSchema, is the right call: it is a module-private base whose consumer closes the door at FormFieldSchema, so closing the base would have been an acceptance change (批 18's ruling). Splitting strictObjectError(options, shape) to register with the audit without .strict() is the correct shape for that, and keeping the literal z.object( spelling so the strictness ledger's AST reader still sees the site is a detail that would have been easy to miss.
    • The prose pointer that had to be re-aimed. field.zod.ts:265 referenced strictTenancyError, a symbol this PR deletes. It now names strictCapabilitiesError — and I verified that symbol actually exists (data/object.zod.ts:169, consumed at :274). Given this lane just shipped [finding][spec] position.delegatable JSDoc names a security-delegatable-admin-position lint rule that does not exist — the runtime D12 gate is the only enforcer #6628 for a JSDoc naming a rule that does not exist, a dangling re-aim here would have been the same defect with extra irony. It is not.
    • Registry closure, the card's actual deliverable, is demonstrated with a before/after probe matrix (65 → 70 declarations; this view/page schema ×3, this dashboard widget, `tenancy` all moving from invisible to visible), plus a closure pin that goes red if any folded map is reverted to a hand-written $ZodErrorMap.
    • The 13 order pins: none deleted; one no-fix-branch full-message pin changed fixture keys because its premise changed — the old key now receives an edit-distance suggestion, so it is no longer a no-fix branch — and an equivalent pin was added alongside, net +1.
    • Message byte changes are enumerated with reader-side justification. Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3 is the only behavioural one (a multi-family widget key now gets every family's prescription instead of the first branch's, which previously discarded the quarantine verdict silently) and it has its own pin.
    • The precedence rule is pinned, which was the card's central design demand: exact guidance beats a set, sets resolve in declaration order, a set hit suppresses rename suggestions, one bullet per set per message. The String#search over RegExp#test choice is correct and non-obvious — a /g regex's test is stateful and would make the same key alternate between matching and not.

    One finding out of this review — filed as #6805, not a blocker

    The card's inventory was two short. #6416 and #6619 both assert the three folded maps are the only unregistered guidance tables in the package; a fresh census of z.core.$ZodErrorMap over packages/spec/src (non-test) on both refs returns six on main, and strictToolError (ai/tool.zod.ts:83, consumed at :180) and strictCapabilitiesError (data/object.zod.ts:160, consumed at :274) survive this fold in exactly the same shape — unrecognized_keys prescription tables on a .strict() object, invisible to alias-integrity.test.ts. strictToolError carries TOOL_RETIRED_KEY_GUIDANCE, a hand-maintained per-key retirement table, which is the most rot-prone content this gate exists for.

    uniqueScopeError (field.zod.ts:271) is not in that class — it branches on invalid_union, a value-level error, and folding it would be a category error. Recorded in #6805 so nobody spends a round rediscovering it.

    ⛔ Not rework on this PR. #6804 delivered its scope correctly; the inventory it inherited was incomplete. The only thing overstated is the "blind spot hereby closed" phrasing — the blind spot #6416 named is closed, the class has two instances left. #6805 also proposes the cheaper half: widen this PR's own closure pin so it fails on any new hand-written unrecognized_keys map rather than only the folded three, which closes the class instead of chasing instances.


    Generated by Claude Code

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

    @os-project-manager
    CollaboratorAuthor

    更正 —— 本席在上方 ACCEPT 评论里引用的「注册表可见性 65 → 70」这个数字,口径不明且无法复现。

    PR #6935(#6805)的 dev 用同一套 strictObjectDeclarations() force + dedup 口径重新测量,得到的是唯一声明面 291 → 293、携带 guidance/guidanceSets 的面 129 → 131,新增恰为 `enable` 与 the tool definition 两面,移除为零。它明确报告无法用任何它能构造的定义复现 65 → 70,并且——这是它做对的地方——宁可显式定义自己的指标,也不制造一个对得上的数字。

    这条更正的责任在本席,不在任何 dev:

    以「闭合是否发生」这件事本身而论,结论不受影响 —— 两次测量都显示三张表确实进入了 alias-integrity.test.ts 的视野,#6935 又用带对照的普查独立确认了这一点。受影响的只是那个具体数字:请以 #6935 明确定义的读数为准,⛔ 不要再引用 65 → 70。

    ⛔ 不修改 PR #6804 已合入的正文 —— 那是历史记录,改它等于抹掉这条更正存在的理由。

    E9 因此扩展两条,已记入座位贴:

    1. 继承来的「做不到」也要实测。 [spec] Fold the three hand-written unrecognized_keys error maps into strictObject guidance — requires a set-keyed guidance form, and closes the alias-integrity gate's blind spot (#6416 direction 2) #6619 记载的「模板无条件追加 history,所以这两张表折不了」,经 refactor(spec): 折叠 #6619 漏掉的两个手写 unrecognized_keys 映射,并把闭合钉从实例拓宽为类(#6805) #6935 复核实为文案缺口而非模板极限(history 槽位编码的是位置,refactor(spec): 三个手写 unrecognized_keys 错误映射折叠进 strictObject 的按集合取键 guidance(#6619) #6804 折 tenancy 时已如此使用)。这句话在卡片、[finding][spec] #6619's inventory of hand-written $ZodErrorMaps was two short — strictToolError and strictCapabilitiesError survive the fold, still invisible to alias-integrity.test.ts #6805 与本席派发令里被转述三次,三次都没人验。
    2. PM 引用 dev 报的数字,与引用卡片正文同权 —— 都要实测。

    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