Filed by the domain:spec @ objectui execution seat (session_01L5xpA5q533BgTTNADibEFt), seat post objectui#5734. ⛔ Filed bare and unassigned — no domain:*, no priority, no type. All three are triage's.
⚠️ Routing input only. The subject is scripts/pm/check-widening-tells.mjs, a governance gate — ⛔ not this seat's surface to edit, and ⛔ this seat did not touch it.
The live blocker
PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9540 → exit 4, C5:
card objectui#9309 (delivering open PR objectui#9540 (draft)) declares Clause-②: no while its diff carries 1 widening tell(s) … packages/types/src/zod/objectql.zod.ts:1811 (T1)
T1 … — a new key on a Zod object schema — the accept set gains a spelling an author may now write — + filter: z.union([
The diff replaces the key in one change block, and the removed value is z.unknown():
export const ObjectGallerySchema = BaseSchema.extend({
- filter: z.unknown().optional().describe('Query filter, forwarded verbatim as $filter'),
+ … comment lines …
+ filter: z.union([
+ z.array(z.any()),
+ z.record(z.string(), z.any()),
+ ]).optional().describe('…'),
z.unknown() is zod's universal acceptor. Every value the union admits was already admitted; 'stage=won' and 42 were admitted before and are refused now. ⇒ the accept set shrinks, and the tell's own sentence — "the accept set gains a spelling an author may now write" — is inverted here.
The asymmetry, measured on this tree with a firing control set
wideningTells([{ filename, status: 'modified', patch }], { repo: 'objectstack-ai/objectui' }), run against scripts/pm/check-widening-tells.mjs at origin/main:
| case |
patch |
result |
| the live one |
- filter: z.unknown()… → + filter: z.union([ (list opens on later lines) |
FIRES T1 |
| control A |
the same removal → + filter: z.array(z.any()).optional()… |
declines |
| control B |
a genuinely new key, nothing removed |
FIRES T1 ✅ |
| control C |
z.enum(['a','b']) → z.enum(['a','b','c']) |
FIRES T1 ✅ |
| control D |
z.enum(['a','b','c']) → z.enum(['a','b']) |
declines ✅ |
⭐ Control A is the finding. The same semantic change — narrowing this key out of z.unknown() — declines when the new value is spelled z.array(z.any()) and fires when it is spelled z.union([…]). The direction is identical; only the spelling of the added value differs.
⚠️ My first run of this probe was VOID and is disclosed rather than dropped: I called wideningTells without { repo }, so every case — including controls B and C, which must fire — declined. T1/T2 are scoped to the contract-source surface, and an objectui path is off-surface when the repo defaults to objectstack. The table above is the re-run in which the controls fire.
Root cause, read in the source rather than inferred
tellsInFile (:1609-1621):
const declaresClosedSet = CLOSED_SET_OPENER.test(text);
const spendable = !declaresClosedSet || respellsExistingClosedSetKey(text, removedOfLine.get(i));
#16943's replacement budget is earned here — the removed filter: line is T1-shaped and buys one T1 unit — and then refused at the spend, because the added line opens z.union(. #17618 replaced the blanket refusal with three facts the block must carry:
a removed line naming the SAME key, both member lists readable on their own line, and the added list a SUBSET of the removed one
Fact 1 holds. Fact 2 cannot: the removed value has no member list at all, because z.unknown() is not a closed set. There is nothing for a subset test to compare against — and that is precisely the case where the comparison is unnecessary, since the removed set is the universe and every replacement is a subset of it by construction.
⇒ the three-fact test is the right instrument for z.enum → z.enum, and has no way to express "the previous value accepted everything".
Why this is worth a card rather than a shrug
Same reason #16943 and #16822 were. The C5 row is unclearable by the honest route — this file's own docblock says so:
a false POSITIVE is repaired in the matcher — ⛔ not paid for by the author … the only word that clears a false tell is Clause-②: no → yes — a widening recorded in a governance ledger that did not happen, and afterwards indistinguishable from one that did. ⛔ Nothing about this row is relaxed by saying so: the exit stays non-zero and stays a hard block.
I have not re-declared, and objectui#9540 is held out of the queue on a diff that only narrows. The seat's reasoning is recorded at objectui#9540 #issuecomment-5672787029.
A remedy shape, offered ⛔ not prescribed
The narrowest reading that fixes this without touching the z.enum → z.enum logic: when the removed line names the same key and its value is z.unknown() or z.any(), the added list is a subset by construction — no member list need be readable. That is the same "positive evidence that the set gained nothing" #17618 asks for, supplied by the removed value's own semantics instead of by a list comparison.
⚠️ Whoever takes it should decide deliberately whether z.any() belongs beside z.unknown() here: both accept every value at runtime, and they differ only in what tsc then permits at the use site. ⛔ I have not measured whether any other spelling (z.custom(), a bare z.ZodTypeAny annotation) belongs in the same class, and ⛔ I am not asserting the list is two.
Prior art — checked before filing, ⛔ not cited from memory
⇒ this is a recurrence of #16943's class in the one shape its successor's three-fact test cannot express, ⛔ not a duplicate of it.
Dedup channel declared: the REST /search/issues endpoint is 403 for this seat; the search above went through the MCP GitHub surface, total_count 12 with 10 returned on one page.
Generated by Claude Code
Filed by the
domain:spec@ objectui execution seat (session_01L5xpA5q533BgTTNADibEFt), seat post objectui#5734. ⛔ Filed bare and unassigned — nodomain:*, no priority, no type. All three are triage's.scripts/pm/check-widening-tells.mjs, a governance gate — ⛔ not this seat's surface to edit, and ⛔ this seat did not touch it.The live blocker
PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9540→ exit 4, C5:The diff replaces the key in one change block, and the removed value is
z.unknown():z.unknown()is zod's universal acceptor. Every value the union admits was already admitted;'stage=won'and42were admitted before and are refused now. ⇒ the accept set shrinks, and the tell's own sentence — "the accept set gains a spelling an author may now write" — is inverted here.The asymmetry, measured on this tree with a firing control set
wideningTells([{ filename, status: 'modified', patch }], { repo: 'objectstack-ai/objectui' }), run againstscripts/pm/check-widening-tells.mjsatorigin/main:- filter: z.unknown()…→+ filter: z.union([(list opens on later lines)+ filter: z.array(z.any()).optional()…z.enum(['a','b'])→z.enum(['a','b','c'])z.enum(['a','b','c'])→z.enum(['a','b'])⭐ Control A is the finding. The same semantic change — narrowing this key out of
z.unknown()— declines when the new value is spelledz.array(z.any())and fires when it is spelledz.union([…]). The direction is identical; only the spelling of the added value differs.wideningTellswithout{ repo }, so every case — including controls B and C, which must fire — declined. T1/T2 are scoped to the contract-source surface, and an objectui path is off-surface when the repo defaults to objectstack. The table above is the re-run in which the controls fire.Root cause, read in the source rather than inferred
tellsInFile(:1609-1621):#16943's replacement budget is earned here — the removedfilter:line is T1-shaped and buys one T1 unit — and then refused at the spend, because the added line opensz.union(.#17618replaced the blanket refusal with three facts the block must carry:Fact 1 holds. Fact 2 cannot: the removed value has no member list at all, because
z.unknown()is not a closed set. There is nothing for a subset test to compare against — and that is precisely the case where the comparison is unnecessary, since the removed set is the universe and every replacement is a subset of it by construction.⇒ the three-fact test is the right instrument for
z.enum→z.enum, and has no way to express "the previous value accepted everything".Why this is worth a card rather than a shrug
Same reason
#16943and#16822were. The C5 row is unclearable by the honest route — this file's own docblock says so:I have not re-declared, and objectui#9540 is held out of the queue on a diff that only narrows. The seat's reasoning is recorded at objectui#9540
#issuecomment-5672787029.A remedy shape, offered ⛔ not prescribed
The narrowest reading that fixes this without touching the
z.enum→z.enumlogic: when the removed line names the same key and its value isz.unknown()orz.any(), the added list is a subset by construction — no member list need be readable. That is the same "positive evidence that the set gained nothing"#17618asks for, supplied by the removed value's own semantics instead of by a list comparison.z.any()belongs besidez.unknown()here: both accept every value at runtime, and they differ only in whattscthen permits at the use site. ⛔ I have not measured whether any other spelling (z.custom(), a barez.ZodTypeAnyannotation) belongs in the same class, and ⛔ I am not asserting the list is two.Prior art — checked before filing, ⛔ not cited from memory
completed— T2 firing on a replaced string property value, same root cause ("the tells are computed on added LINES and have no way to see that a line replaced an identical-typed one"), fixed by the replacement budget in PR fix(pm): read a widening tell as a NET member/key delta, so a replaced line stops reading as an addition #17216.BARE_STRING_ELEMENTfalse C5 that first priced this class.Served-tier:line is refused only beside a gate clear (C7) — a non-gated pair with the same record passes exit 0, against contract-review.md :29 「无此行不成裁决」 #18174 / [finding] check-clause2-carriers: a contract-review record whose head sha shares one code span with its key names NO head (H51_SHA_SPAN), so C6 reads 「no review of record」 where one exists — contract-review.md :28 never says the span holds the sha alone #18141 — opencheck-clause2-carriersfindings, all on different rows (epic-tracker carrier demand,Served-tier:legibility, head-sha span). ⛔ None covers the C5 budget spend.⇒ this is a recurrence of #16943's class in the one shape its successor's three-fact test cannot express, ⛔ not a duplicate of it.
Dedup channel declared: the REST
/search/issuesendpoint is 403 for this seat; the search above went through the MCP GitHub surface,total_count12 with 10 returned on one page.Generated by Claude Code