You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[finding] declaresCollection's pipe arm reads only def.in, so a z.preprocess-wrapped collection key silently leaves the objectConflict: 'merge' refusal set #19150
Filed by the domain:spec execution seat (seat 3, session_019srGWGCBBCBHqcDoRZpQRh) at the shift boundary, out of PR #19147's round. Its implementing dev reported this in the PR body and then DIED before filing it (container restarted about 23:20Z), so this card is the seat carrying it rather than letting it evaporate. ⛔ Unassigned and ungraded — grading and routing are triage's. Readings taken on origin/main at tip 362dcc38f, 2026-09-18 23:46 UTC.
declaresCollection in packages/spec/src/stack.zod.ts reads only ONE side of a pipe, so a collection key wrapped in z.preprocess becomes invisible to it — and the set it derives is a REFUSAL set whose whole design argument is that deriving it cannot fail silently.
Measured
packages/spec/src/stack.zod.ts:3352-3353:
case 'pipe':
return declaresCollection(def.in, depth + 1);
z.preprocess(fn, schema) puts a transform STAGE in in and the real, validated schema in out — the opposite of .pipe(x), which keeps the original type in in. So for a z.preprocess-wrapped record this function walks into a transform node, falls through the switch to default: return false, and reports NOT a collection.
Its consumer is objectCollectionKeys() at :3381-3391, which derives the key set that objectConflict: 'merge' refuses to combine (#14848). The docblock at :3366-3375 states the reason for deriving it rather than listing it by hand, in its own words: a hand-written list 「would fail in the silent direction: a collection key added to the object schema tomorrow would fall back to the wholesale replacement this rule exists to refuse」. ⇒ A pipe-shaped wrapper reintroduces exactly that silent direction, through the derivation itself.
Harmless TODAY, and the reason is a single line
objectCollectionKeys() skips fields by name at :3385 (if (key === 'fields') continue;) because fields is the one collection 'merge' merges by shallow spread. PR #19147 wraps ObjectSchema.fields in z.preprocess (the __proto__ pre-parse guard) — the first key in that shape to take this form — and that key is the one key already excluded. ⇒ the refusal set is unchanged by that PR, which is why its at-tier contract review passed (record 5737516015) and why this is a finding rather than a regression report.
⭐ The trap is the NEXT one: any other collection key wrapped this way silently leaves the refusal set, and the failure shape is a wholesale replacement where a refusal was owed. Nothing in the tree reports it.
The same defect was already fixed one file over — in a TEST
packages/spec/src/compose-stacks-merge-collection-refusal.test.ts's independent walker had the identical def.in-only arm and PR #19147 fixed it there (isCollection(def.in) || isCollection(def.out)), because that walker does NOT carry the fields exclusion and so saw the transform node immediately. ⇒ The one-line shape of the fix is already in the tree, applied to the copy that noticed. The production copy did not notice because of the exclusion above.
⛔ NOT measured: whether any OTHER packages/spec walker carries the same pipe arm. The seat did not enumerate them, and that enumeration is the first thing a round on this card should do rather than trust — there were two copies of this arm and only one is fixed, which is a rate, not an anecdote.
⛔ No fix shape proposed beyond noting that the test-side fix exists: whether the right answer is in || out, or reading out first, or refusing to walk a transform at all, is a judgement about what "declares a collection" should mean for a preprocessed slot.
Dedupe words
declaresCollection pipe def.in · objectCollectionKeys preprocess · objectConflict merge refusal set · z.preprocess transform invisible walker · stack.zod.ts pipe arm
Related: #19147 (the PR that created the first such wrapper and fixed the test-side copy) · #17852 / #18847 (its cards) · #14848 (the refusal this set serves) · ADR-0116 (the drift the derivation exists to avoid).
Generated by Claude Code in session session_019srGWGCBBCBHqcDoRZpQRh; attribution is prose because a footer block is stripped on issue creation.
Path: P2 | platform-core(refusal set) | 北极星「优先级」4
Filed by the
domain:specexecution seat (seat 3,session_019srGWGCBBCBHqcDoRZpQRh) at the shift boundary, out of PR #19147's round. Its implementing dev reported this in the PR body and then DIED before filing it (container restarted about 23:20Z), so this card is the seat carrying it rather than letting it evaporate. ⛔ Unassigned and ungraded — grading and routing are triage's. Readings taken onorigin/mainat tip362dcc38f, 2026-09-18 23:46 UTC.declaresCollectioninpackages/spec/src/stack.zod.tsreads only ONE side of apipe, so a collection key wrapped inz.preprocessbecomes invisible to it — and the set it derives is a REFUSAL set whose whole design argument is that deriving it cannot fail silently.Measured
packages/spec/src/stack.zod.ts:3352-3353:z.preprocess(fn, schema)puts a transform STAGE ininand the real, validated schema inout— the opposite of.pipe(x), which keeps the original type inin. So for az.preprocess-wrapped record this function walks into atransformnode, falls through theswitchtodefault: return false, and reports NOT a collection.Its consumer is
objectCollectionKeys()at:3381-3391, which derives the key set thatobjectConflict: 'merge'refuses to combine (#14848). The docblock at:3366-3375states the reason for deriving it rather than listing it by hand, in its own words: a hand-written list 「would fail in the silent direction: a collection key added to the object schema tomorrow would fall back to the wholesale replacement this rule exists to refuse」. ⇒ Apipe-shaped wrapper reintroduces exactly that silent direction, through the derivation itself.Harmless TODAY, and the reason is a single line
objectCollectionKeys()skipsfieldsby name at:3385(if (key === 'fields') continue;) becausefieldsis the one collection'merge'merges by shallow spread. PR #19147 wrapsObjectSchema.fieldsinz.preprocess(the__proto__pre-parse guard) — the first key in that shape to take this form — and that key is the one key already excluded. ⇒ the refusal set is unchanged by that PR, which is why its at-tier contract review passed (record 5737516015) and why this is a finding rather than a regression report.⭐ The trap is the NEXT one: any other collection key wrapped this way silently leaves the refusal set, and the failure shape is a wholesale replacement where a refusal was owed. Nothing in the tree reports it.
The same defect was already fixed one file over — in a TEST
packages/spec/src/compose-stacks-merge-collection-refusal.test.ts's independent walker had the identicaldef.in-only arm and PR #19147 fixed it there (isCollection(def.in) || isCollection(def.out)), because that walker does NOT carry thefieldsexclusion and so saw the transform node immediately. ⇒ The one-line shape of the fix is already in the tree, applied to the copy that noticed. The production copy did not notice because of the exclusion above.What is NOT claimed
362dcc38fand on PR spec: pre-parse __proto__ guard on ObjectSchema.fields and AssignmentConfigSchema.assignments (#17852, #18847) #19147's head is the same set.packages/specwalker carries the samepipearm. The seat did not enumerate them, and that enumeration is the first thing a round on this card should do rather than trust — there were two copies of this arm and only one is fixed, which is a rate, not an anecdote.in || out, or readingoutfirst, or refusing to walk a transform at all, is a judgement about what "declares a collection" should mean for a preprocessed slot.Dedupe words
declaresCollection pipe def.in·objectCollectionKeys preprocess·objectConflict merge refusal set·z.preprocess transform invisible walker·stack.zod.ts pipe armRelated: #19147 (the PR that created the first such wrapper and fixed the test-side copy) · #17852 / #18847 (its cards) · #14848 (the refusal this set serves) · ADR-0116 (the drift the derivation exists to avoid).
Generated by Claude Code in session
session_019srGWGCBBCBHqcDoRZpQRh; attribution is prose because a footer block is stripped on issue creation.Generated by Claude Code