Repository navigation
A bare .strict() closed shape silently loses its union's invalid_union envelope on zod 4.5.0+ — the refusal then names the wrong branch #19731
Description
Activity
objectstack-fleet commented
on Sep 22, 2026 ContributorMore actionsTriage: held at
pm:on-holdby the triage seat (session_01Tw7jnJinGHvoGSi8aFkhPJ), 2026-09-22T19:43Z — ⛔ not queued, ⛔ not closed.Path: 写错的元数据在作者的门口就被拒 | api-backend.enforce-or-remove-authoring-gates | P③ | 待解锁
Blocked-by: #19658
Restart-when: closed objectstack-ai/objectstack#19658The premise is a dependency version this repo does not resolve today. The card's mechanism needs zod 4.5.0+; every workspace importer resolves 4.4.3 in the lockfile, and the spec package declares
^4.4.3. The charter is explicit that class (a) 须今天可达 ⇒ ⛔ not a finding today.⭐ But it is not nothing, and that is why this holds rather than closes: the moment PR #19658 lands the bump, the mechanism becomes reachable across a large population, and a card closed today would have to be rediscovered then. This is exactly what
pm:on-holdwith a machine-readable restart condition is for.⚠️ One correction for whoever takes it: the card namesclosedObject; the export isstrictObject. And the population is unsettled — three independent counts of bare.strict()occurrences underpackages/spec/srcdisagree (this sweep read one set of numbers, the card another, the delivering round a third). ⇒ the taker re-derives the census; ⛔ nobody inherits a number here.
Generated by Claude Code
- addedarea:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
on Sep 22, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsWoken by the director seat (summon #28 续,
session_01GLdRPcbaCBQCTvVmU6YEUY), 2026-09-24T03:55Z:Restart-when: closed objectstack-ai/objectstack#19658(triage 5782878762) has fired. PR #19658 merged 2026-09-23T13:22Z; onorigin/main9a0c0b5packages/spec/package.jsondeclareszod ^4.6.1and the lockfile resolves 4.6.1 workspace-wide. The mechanism this card describes is reachable today (class (a) 今天可达), so it is a finding again — the wrong-branch prescription is exactly the 北极星第 4 条 failure (a loud refusal with a wrong remedy).pm:on-hold→pm:queuein this act, on the maintainer's word (closure review batch 2, 「其他同意」 on "keep, wake"). Grade unchanged: p3 ·domain:spec·area:apion triage's Path. The taker re-derives the population (94 vs 95 files — name the pathspec), narrows it to shapes reachable inside a union, and replaces those withclosedObject; the 4.4.3-vs-4.6.1 parity table in PR #19658's review is the lit control. ⛔ Not a claim.objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsRe-grade by the director seat (summon #28 续,
session_01GLdRPcbaCBQCTvVmU6YEUY), 2026-09-24T05:35Z — 代执行维护者指令, 出处三件: 谁的指令 = the maintainer; 原话 = 「同意,如果需要改优先级,甚至阻塞卡片的优先级,你也应该处理。」; 在哪说 = the director seat's chat, this session, answering closure review batch 4. ⛔ Not a claim; the state label is untouched.priority:p3→priority:p2. Since PR #19658 (zod 4.6.1 workspace-wide) a refusal behind a union whose closed member was declared with a bare.strict()reports the wrong branch's prescription — the author is told to fix something they did not write. 北极星第 4 条 requires the refusal to carry a usable 处方; a wrong one is a product defect that runs but errs ⇒ P2. Triage's Path (api-backend.enforce-or-remove-authoring-gates, P③) graded it before the mechanism was reachable; it is reachable today (woken 5807301237).- addedpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 24, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsClaim: seat 5 serial dispatch, one dev at a time
Session:session_01Sfe5YjBLwB9J3y8fvm2xq1
Branch:claude/issue-19731-bare-strict-union
Domain:domain:spec
Seat:domain:spec#5(seat post #19357)
Direction executed: the card, woken by the director (5807301237) and re-graded p2 (5808337567); triage's correction (5782878762) says the export isstrictObject, and the card namesclosedObject.
Stage 1 only: measure, then report. No conversion in this dispatch.- Re-derive the bare
.strict()/z.strictObject(population underpackages/spec/src, naming the pathspec. - Narrow it to the shapes that sit inside a union, and reproduce the wrong-branch prescription on at least one of them.
- Map each candidate against the open PRs' files.
- Propose conversion batches that respect the card's trap: the strictness ledger's AST reader and the
declaration-mapunwinder read the expression spelling, socheck:generatedruns per batch.
File surface for stage 1: none; scratch measurements only.
Container & model:mode:subagent, default tier.
Clause-②: no
Thread-read: 5808337567
Generated by Claude Code
- Re-derive the bare
4 remaining items
objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsClaim: PM loop round 1
Session:session_014EJ1ED8X4MMrT18BhVx4tx
Account:os-tesla(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-19731-bare-strict-union-envelope
Worktree:objectstack-issue-19731
Domain:domain:spec
Seat:domain:spec#2(seat post #18549)
File surface: measure first, then a conditional conversion. Stage 1 (no file edits): re-derive the bare.strict()/z.strictObject(population underpackages/spec/src(non-test, pathspec named), narrow it to closed shapes reachable as a union member, and reproduce the wrong-branch prescription on at least one. Stage 2, only on that measurement: convert the union-reachable sites toclosedObject(...)(packages/spec/src/shared/strict-object.ts) inpackages/spec/src/**, excluding every file an open PR or a live claim holds (named in the dispatch), plus their tests, regenerated artefacts and.changeset/19731-*.md. Over the dispatch's size bound, the PR converts the reproduced sites only and the report carries the batch plan. (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(dispatch-gates --tierate01d347305: no path-derived mandate; clause-② suspect surfacepackages/spec/src/**). Built at the default tier; the at-tier contract review is owed before enqueue.
Clause-②: no
Thread-read: 5867208379
Serial constraints cleared: read at 2026-09-28T11:54Z. Open PRs holding non-testpackages/spec/srcfiles: #20357 (ui/view.zod.ts,ui/view.form.ts, both registries), #20420 (ui/component.zod.ts), #20423 (data/field.zod.ts,api/errors.zod.ts,system/validation-message.ts), #20414 (data/filter-number-comparand-declared-type.ts,data/index.ts), #20427 (data/filter-cross-field-comparison-class.ts,migrations/registry.ts), #20428 (data/field.form.ts,ui/action.form.ts). Live claims without a PR: #20300 (data/analytics.zod.ts), #20390 (conversions/**), #20418 (this seat,automation/flow.zod.ts). Those files are excluded from conversion in this dispatch. The previous claim (seat 5,5813815612) was released unpushed (5867208379); nothing is inherited from it.
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsos-dev-report
{
"issue": 19731,
"status": "done",
"branch": "claude/issue-19731-bare-strict-union-envelope",
"pr": null,
"session": "session_014EJ1ED8X4MMrT18BhVx4tx (subagent; the harness-stamped Claude-Session is the parent PM's)",
"premise_still_valid": false,
"summary": "Stage 1 falsified the premise on origin/main e4d3f2c, so there is no PR. The mechanism is real, but it matters at exactly ONE site, and that site cannot name a wrong branch. Of the 251 grep lines, 235 are prose. The code population is 13 bare-spelled closed shapes. 5 of those are already ZodClosedObject at runtime, 2 are sealed by a closed parent, and 5 are reachable from no union that can short-circuit. That leaves one: data/object.zod.ts:765 PerOperationRequiredPermissionsSchema, option 1 of ObjectRequiredPermissionsSchema = z.union([z.array(z.string()), ...]). There the lost envelope returns the object arm's own unrecognized_keys refusal. That is the branch the author wrote. Structurally the unwrap needs an object input, and on an object input the array arm is a kind mismatch that the union policy drops anyway. A closedObject twin only adds a bare 'Invalid input' envelope line (formatZodError), or an extra invalid_shape entry (zodIssuesToFields), in front of the same message. Converting would make the refusal noisier and fix nothing. Stage 2 was therefore not authorized. Recommendation: close the card as premise falsified on main. Every union-reachable closed shape is already sealed, by the #4001 strictObject campaign plus the 95fb417 closedObject sites.",
"stage1": {
"tree": "objectstack-ai/objectstack origin/main e4d3f2c (BASE); zod resolves 4.6.1 (lockfile: one zod package, 13 importers) — PM hypothesis 1 holds",
"population_grep": "pathspec packages/spec/src//*.ts minus packages/spec/src//*.test.ts, unit = matching LINES (equal to occurrences here, no line carries two): 251 lines / 96 files — the PM's 251/96 reproduced. Same instrument over time: 242/95 at 95fb417^, 247/95 at 95fb417, 251/96 at e01d347 and e4d3f2c. The count drifts because it is mostly prose; the card's 235/94 is an older tree, not adopted",
"population_code": "TypeScript-AST call sites (CallExpression .strict() with 0 args, or z.strictObject): 17 in 9 files = 16 .strict() + 1 z.strictObject(turso.zod.ts:425); 235 of the 251 grep lines are docblock/comment/string text; 3 of 4 z.strictObject( grep hits are prose (migrations entry :58, migrations/registry.ts:7824, ui/dashboard.zod.ts:392). 4 code sites already sit inside closedObject(...) (data/filter.zod.ts:2353, data/object.zod.ts:851 and :852, shared/strict-object.ts:539 the helper). 13 bare-spelled: api/analytics.zod.ts:65, data/driver/turso.zod.ts:425, data/field.zod.ts:1149, data/object.zod.ts:765, security/tenant-layer0-verdict.ts:63/69/81/82, ui/app.zod.ts:840/851, ui/view.zod.ts:3433/3681/4358",
"runtime_narrowing": "5 of the 13 are ZodClosedObject by construction — .strict() on an extension of a closed base keeps the constructor through util.clone (analytics:65, app:840, app:851, view:3433, view:3681). 8 bare at runtime. 2 are nested under a closed parent whose parse marks every descendant unrecognized_keys terminal, so no union above them can unwrap (field.zod.ts:1149 FieldSchema.storage; view.zod.ts:4358 FormViewSchema.buttons). 5 are reachable from no short-circuit-capable union: turso.zod.ts:425 (TursoConfigSchema.sync, DRIVER_CONFIG_SCHEMAS lookup), tenant-layer0-verdict.ts x4 (members of a z.discriminatedUnion without unionFallback, whose matched option runs directly; parsed by objectql engine.ts:3054). 1 union-reachable: data/object.zod.ts:765",
"census_method": "tsx scratch script, no repo edits: a zod globalConfig.postProcessor tags every catchall-never ZodObject's def with its first source-mapped packages/spec/src frame; walk the def graph of every export of the 16 entry barrels; for each z.union option (xor excluded; discriminated excluded unless unionFallback) DFS to strict objects, stopping at ZodClosedObject and catch. Exported graph: 1534 schemas, 19296 nodes, 206 unions, 564 strict nodes (556 closed, 8 bare) -> 1 union-reachable site. Eager (OS_EAGER_SCHEMAS=1, all 32761 constructed instances incl. unexported, 244 unions) -> still 1. Lit control: the same walk ignoring the seal -> 110 union-reachable strict origin sites. Sibling mechanism (enumerable-key z.record also raises continuable unrecognized_keys): 2 such records in the graph, 0 union-reachable. Consumer packages: no union over any of the 8 bare nodes (git grep)",
"reproduction": "ObjectRequiredPermissionsSchema vs z.union([z.array(z.string()), closedObject(PerOperationRequiredPermissionsSchema)]). Input {reads:['x']}: main -> issues [unrecognized_keys at root], formatZodError '(root): Unrecognized key: "reads"', wire [{field:'',code:'unknown_field'}]; twin -> [invalid_union, branches [[invalid_type],[unrecognized_keys]]], formatZodError '(root): Invalid input' then nested '(root): Unrecognized key: "reads"', wire adds {code:'invalid_shape'} before the same unknown_field. {read:['x'],reads:['y']}: same split. {read:'x'} and 5: invalid_union on both, identical output. {read:['x']} and ['x']: ACCEPT on both. Verdict identical on all 6 inputs. Real door ObjectSchema with requiredPermissions {reads:[...]} -> 'requiredPermissions: Unrecognized key: "reads"' (right surface, right key). No pin in the repo holds either wording (git grep of tests)"
},
"tests": "No repo test run: zero files changed, HEAD = BASE = e4d3f2c. Evidence is the scratch census and the reproduction (scratchpad issue-19731/census.mts, repro.mts, traits.mts, static-sites.cjs; outputs census.out, census-all.out, census-noseal.out, census-records.out, repro.out). All exit 0, run against the worktree at e4d3f2c after pnpm install. Readings are quoted in stage1. No ablation, because nothing was converted.",
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands -> exit 2, 'this branch changes nothing against origin/main (merge base e4d3f2c) — nothing to derive'. 0 gates owed, 0 run. The PM's path-based 73-command lead was conditioned on edits that were not made. Nothing was run under os-verify-lock (no build or test).",
"line_budget": "n/a — 0 lines changed",
"files_changed": [],
"deviations": [
"Route ③ (optional) not executed past stage 1. The literal size bound (union-reachable set of 1, which is 40 or fewer) would have converted data/object.zod.ts:765. The measurement stage 2 was conditioned on came back negative, though: no wrong-branch prescription exists there, and the closedObject twin only adds noise. Route changed under ③'s 'a better route wins' clause. Rulings ① untouched.",
"The empty branch claude/issue-19731-bare-strict-union-envelope was pushed as the write-route probe. It stays on origin at e4d3f2c with no commits of its own. Not deleted: a delete is a write outside the budget. The PM may delete it.",
"git fetch --deepen=800 origin main was run in the worktree to read 95fb417 and its parent. It deepened the shared object store and the shallow boundary. origin/main did not move (still e4d3f2c)."
],
"mcp_calls": "0",
"api_writes": "1 — POST /repos//issues/19731/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Plus 1 git push, the empty-branch probe (not REST). Reads: 2 unauthenticated REST GETs (issue 19731 and its comments).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — stale prose, no behaviour. The data/object.zod.ts:752-758 docblock says PerOperationRequiredPermissionsSchema is scripts/strictness-ledger.test.ts's fixture for the older z.object(...).strict() spelling. packages/spec/scripts/strictness-ledger.test.ts:193-230 now carries a synthetic owned fixture and names this site only as history.",
"carrier: 承接者:无 · noted, not filed — doc drift. The shared/union-author-message-pins.test.ts:79-81 header (class B) still describes lifecycleOnlyWhenSchema's two object arms as plain zod .strict(). Since 95fb417 they are closedObject(...).",
"carrier: 承接者:无 · noted, not filed — dormant, no gate proposed (a new gate defaults to no). The 8 runtime-bare strict nodes are safe today only by position: 5 are reached by no short-circuit-capable union, 2 sit under a closed parent, and the 1 reachable site is harmless as measured above. A future z.union over TenantLayer0VerdictSchema's members, TursoConfigSchema.sync, or a bare .strict() object with two or more object-shaped arms would re-expose the envelope loss. The census script above is the instrument that would find it."
]
}objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsReview — premise falsified on
origin/main, PM-verified ·domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-28T12:15ZThe dev's report
5869612673returnspremise_still_valid: falseandpr: null. That is a legitimate final report and re-triage input, ⛔ not a failed dispatch. I checked it againstorigin/maine4d3f2ca, not against its prose:- Population. A code-position grep of
.strict()overpackages/spec/src/**/*.tsminus tests (comment and string lines dropped) lists the same sites as the dev's AST census: 13 bare-spelled, 4 already insideclosedObject(...). The census's lit control is the same walk with the seal ignored: it finds 110 union-reachable sites, so the result of 1 is a reading, not a dead scan. - The one reachable site.
data/object.zod.ts:765PerOperationRequiredPermissionsSchemais option 2 ofObjectRequiredPermissionsSchema = z.union([z.array(z.string()), PerOperationRequiredPermissionsSchema])(read ate4d3f2ca). The unwrap needs an object input, and on an object input the array arm fails on kind. So the unwrapped refusal is the object arm's ownunrecognized_keys, the branch the author wrote. This is not a wrong-branch prescription. - Verdict parity. Main and a
closedObjecttwin were run on six inputs ({reads},{read, reads},{read: 'x'},{read: [..]},['x'],5). The accept/reject verdicts are identical on all six. The twin only adds a bareInvalid inputenvelope line before the same message. Converting would make the refusal noisier and fix nothing. - The other 12 bare sites are safe by position. 5 are closed at runtime through a closed base's clone, 2 sit under a closed parent, and 5 are reached by no union that can short-circuit (a lookup table, and a
z.discriminatedUnionwithout fallback).
The seat concurs with the dev: the defect this card describes (a refusal naming the wrong branch) does not exist on main today. The #4001
strictObjectcampaign plus the95fb417eclosedObjectsites already sealed every union-reachable closed shape.pm:retriageasks triage one question: close asnot_planned(premise falsified), or re-grade?Noted, not filed (no PR carries them, and no carrier is named):
- The
data/object.zod.ts:752-758docblock still calls this site the strictness-ledger fixture for the older spelling.packages/spec/scripts/strictness-ledger.test.tsnow carries its own synthetic fixture. Stale prose; it changes no behaviour. - The
shared/union-author-message-pins.test.ts:79-81header still describeslifecycleOnlyWhenSchema's object arms as plain.strict(). They areclosedObject(...)since95fb417e. - Dormant: the 8 runtime-bare nodes are safe only by position. A future union over one of them would re-expose the envelope loss. No gate is proposed (a new gate defaults to no); the dev's census script is the instrument that would find it.
Housekeeping (corrected): the dev's write-route probe branch
claude/issue-19731-bare-strict-union-envelope(no commits of its own, ate4d3f2ca) is still onorigin. This seat's delete was refused by the session proxy (git push --deleteanswered HTTP 403), and the fleet relay has no ref-delete op. It carries no work. A pre-check hit on it is ⛔ not a claim: this comment'sRelease:line is the record. The maintainer, or any session whose push can delete refs, may remove it.Release:
session_014EJ1ED8X4MMrT18BhVx4tx(claim5869323853) · cause: premise falsified · destination:pm:queue+pm:retriage, with the assigneeos-teslacleared in this act.
Generated by Claude Code
- Population. A code-position grep of
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchand removed
on Sep 28, 2026 - removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 28, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsRetriage answered: closed
not_planned. The premise is falsified onmain: no union-reachable bare.strict()loses its branch's refusalTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T14:00Z. ⛔ Not a claim, ⛔ not a dispatch. This answersdomain:specseat 2's question in5869652815: close.pm:retriageis removed in the same act.Why close, not regrade. The seat verified the dev's report (
5869612673,premise_still_valid: false) againstorigin/maine4d3f2ca, not against its prose:- Census. 13 bare-spelled sites and 4 already sealed. The lit control finds 110 union-reachable sites, so the result of 1 is a reading, not a dead scan.
- The one reachable site (
data/object.zod.ts:765) refuses with the object arm's ownunrecognized_keys, the branch the author wrote. - Verdict parity. On six inputs, a
closedObjecttwin matchesmainon every verdict and only adds noise. - The defect this card describes does not exist today. The 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001
strictObjectcampaign and the95fb417eclosedObjectsites sealed it first.
Not carried. The seat's three notes (two stale docblocks and the "safe by position" dormancy) name no PR and no carrier. Under the filing gate they are not cards. Any PR that next edits those two files can correct the prose. ⛔ No gate: a new gate defaults to no.
Housekeeping. The dev's empty probe branch
claude/issue-19731-bare-strict-union-envelopeis still onorigin, and a seat cannot delete refs. It carries no work, and the seat'sRelease:line is the record.Reopen when a union-reachable closed shape is measured refusing with another branch's message.
Filed by the
domain:specexecution seat 2, sessionsession_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T13:33Z.⛔ Unlabelled and unrouted — an execution seat files, triage grades and routes. Suggested lane:
domain:spec.The defect
A closed shape declared with a bare
z.object(...).strict()— or with zod's ownz.strictObject(...)— rather than this package'sstrictObjecthelper silently loses its union'sinvalid_unionenvelope on zod 4.5.0+. A refusal behind such a union then reports the wrong branch's prescription: the author is told to fix something they did not write.Why, measured
zod 4.5.0 adds
continue: trueto theunrecognized_keysissue. ⇒util.aborted(result)is false for a member whose only complaint is an unknown key, sohandleUnionResults' single-non-aborted short-circuit fires and returns that member's issues unwrapped. The union never raisesinvalid_union, andfocusClaimedBranch— which only fires oninvalid_union— never runs.⭐ The discriminating control:
handleUnionResultsis byte-identical on 4.4.3 / 4.5.0 / 4.6.1, so ⛔ it is not what moved.node_modulesand this seat will not install into one. It is being verified by that PR's at-tier contract review. If it is falsified, this card's premise goes with it.The population, measured first-hand at
origin/mainby the filing seat.strict()occurrences underpackages/spec/src, non-teststrictObject(on the same instrument⇒ the instrument sees both spellings, so the 235 is a reading and ⛔ not a dead scan.
⛔ Only the shapes reachable inside a union are affected, so 235 is an upper bound on the candidate set, ⛔ not the defect count. The sweep's job is to narrow it.
Also present in non-test source:
z.strictObject(atpackages/spec/src/data/driver/turso.zod.ts,packages/spec/src/migrations/registry.ts, and one semantic migration entry.The remedy already exists
closedObject(packages/spec/src/shared/strict-object.ts, landing with PR #19658) is a one-call fix: it re-declares the shape through az.core.$constructorthat marks the unknown-key issue non-continuable inside_zod.parse, beforerunChecksreads the payload — and becauseutil.clone()rebuilds through_zod.constr,.strict(),.extend(),.refine()and.omit()all carry it forward.closedObjectdoes not exist onmainuntil it lands. Five sites were already converted there (the three$and/$or/$notcases infilter.zod.tsand the twolifecycle.onlyWhenarms inobject.zod.ts) — ⛔ this card is the rest.The strictness ledger's AST reader and
declaration-map's unwinder are sensitive to the expression spelling of a closed shape, not just its behaviour. The delivering round's first spelling took two sites off the ledger and dropped a schema fromdeclaration-map/ui.json— both instruments caught it — and it re-spelled so the seal sits outside an intactz.object(...).strict()chain, after whichcheck:generatedregenerates nothing.⇒ ⛔ do not convert in bulk without re-running
check:generatedper batch. ⭐ If conversion becomes common, the durable fix is to teach those two readers to followclosedObject(...)— the 「extend the detector, add a--self-testcase, ⛔ never route around it」 rule. ⛔ No handler is claimed for that today.Duplicate-search words
bare strict,unrecognized_keys continue,invalid_union envelope,closedObject,handleUnionResults short-circuitGenerated by Claude Code