Repository navigation
types(imported-defaults): stripImportedDefaults drops the .describe() of every imported default — 110 of 110 described ZodDefault members lose it across spec 17.4.0, 0 survive #9034
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
on Sep 10, 2026 Claim: session
session_01Jmxdo7bmeqCQHLSfmLVX9w(PM seatdomain:spec@objectui) → dev subagent on branchclaude/issue-9034-imported-defaults-describe.Clause-②: **yes**.⭐ Declaredyesunder the charter's own tie-break — unsure ⇒ declareyes— ⛔ not because a widening tell was found. My honest reading is that nothing here widens: restoring a dropped.describe()adds no key, no closed-set member, no published export and no registration, and it moves objectui toward the protocol rather than away from it. But this lands inpackages/types/src/zod/, the contract mirror surface, and it changes what every published schema object carries — a reviewer could reasonably call that a contract-surface change, and I am not confident enough to declareno.⇒
needs:contract-reviewis now on this card, and the dual carrier must AGREE: ⭐ hang it on your PR too. (The rule is make the PR carrier MATCH the card carrier — here they must both be present.) An in-seat ceiling-tier contract review is owed on your final head before this can land; ⛔ do not flip ready, ⛔ do not enqueue.⭐ The premise is re-derived and it is STRONGER than the card states
Measured on
origin/mainbefore dispatch, so you inherit a live reading rather than a filed one:packages/types/src/zod/imported-defaults.ts— HTTP 200, 326 lines.case 'default': {at:200, and:201is the subject:const inner = walk((schema as unknown as { removeDefault: () => z.ZodType }).removeDefault());
- Firing control that the pattern matches here:
grep -c "case '"→ 16 matching lines,grep -o "case '" | wc -l→ 16 occurrences. ⭐ Both spellings stated, because a bare-cis a count of lines containing and is not a corpus control on its own.
⭐ The finding the card did not have.
describe|descriptionoccurs on exactly one line in the whole file —:167— and it is thelazyarm's own prohibition:⛔ Rebuilt through
cloneWithDef, notz.lazy(…): a freshz.lazywould be a different class with none of this node's owndef.checksor description, which is the same silent-loss shape the clone rule exists for.⇒ This file already names description loss as the defect, and already has a rule against it —
cloneWithDef. Thelazyarm obeys that rule. Thedefaultarm at:201does not:.removeDefault()returns the inner type and discards the outer node, which is where the protocol's.describe()lives. So the card is not asking for a new principle; it is asking why one arm is exempt from the file's own stated rule.⛔ Do not take that framing as settled. Confirm or refute it yourself — in particular, check whether
.removeDefault()genuinely drops the description or whether zod propagates it to the unwrapped node. ⭐ If zod propagates it, the card is refuted and the right deliverable is that measurement, not a patch. Report a refutation exactly as readily as a fix.What is owed
- A measurement of the population before any patch: how many imported schemas carry a
.describe()on aZodDefaultnode today, and how many of those descriptions survivestripImportedDefaults. The seat measured 110/110 when filing; ⛔ re-derive rather than trusting that number, and state the exact commands. - If confirmed: make the
defaultarm preserve the outer node's description, ⭐ using the file's owncloneWithDefrule rather than inventing a second mechanism — the file's whole argument is that there is one rule. ⚠️ The identity property is load-bearing and must not break. The docblock below:175states it: "A node is rebuilt ONLY if the walk actually changed something beneath it. A subtree with noZodDefaultin it therefore comes back REFERENCE-EQUAL to the spec's own object." ⛔ A fix that rebuilds nodes it did not need to rebuild breaks that property even while the tests pass on values. Pin it.- A firing control for the fix: an imported schema whose
.describe()is shown absent before and present after, plus a control on a schema with no default whose identity is shown preserved across the change.
Standing rules — read every line, they are not boilerplate
Identity. The card is already assigned and this
Claim:is yours; it names your branch. ⛔ Never write the assignee field. ⛔ Never post a secondClaim:. If the newestClaim:names another branch, stop and report.Tools that do NOT exist in objectui — ⛔ never wait on a red from these; they are objectstack-side and the seat runs them:
scripts/pm/os-regen-merge.sh·scripts/pm/check-widening-tells.mjs·scripts/pm/check-clause2-carriers.mjsContainer hazards.
/home/user/objectuiis a SHALLOW checkout with a STALEorigin/main. ⛔ Nevergit fetch, ⛔ nevergit checkout, ⛔ never cut a worktree there. Clone fresh into your own scratchpad subdirectory./home/user/objectstackis a shared primary checkout — read-only.- ⛔ Never
git stash. ⛔ Never kill a process by name. ⛔ Never mutatenode_modules. ⛔ Never touchcontent/docs/releases/.
Measurement discipline — the seat enforces this hardest.
- ⛔ Never write "(no output above = clean)" or any unconditional false-clean echo. Print the command and the exit code; let the output speak.
- ⭐ A control is only a control if it is known to fire in the region being tested.
⚠️ Two true negatives are not a reading — the seat made exactly that error twice tonight and both times a dev caught it. - ⛔ A bad probe value is not a reading. ⛔ An empty gate section is a FAILED RUN, not a clean one.
- ⛔ Never quote a bare
grep -cwithout its exact command. - Read the stored body back after every GitHub write and report what came back.
Deliverable. Worktree → code → tests → changeset → push → draft PR carrying
needs:contract-review. Report structured JSON: PR number and head sha, the population measurement with exact commands, the confirm-or-refute verdict on.removeDefault(), the identity-property pin, both control readings, and CI as total / success / skipped / non-green plus whethertotal_countequals the returned array length.
Generated by Claude Code
Claim: session
session_01Jmxdo7bmeqCQHLSfmLVX9w(PM seatdomain:spec@objectui) → dev subagent on branchclaude/issue-9034-imported-defaults-describe
Session:session_01Jmxdo7bmeqCQHLSfmLVX9w
Branch:claude/issue-9034-imported-defaults-describe
Clause-②: yes⚠️ FORMAT REPAIR of my own earlier claim on this card — ⛔ NOT a re-claim and ⛔ NOT a reassignment. Same session, same branch, same declaration. The earlier claim comment stands for its content; this one exists because that content was written in a shape the enqueue gate cannot read.What was wrong, and how it was caught
scripts/pm/check-clause2-carriers.mjs --pairreturned exit 4 on a sibling PR with the finding "theClause-②declaration is MISPLACED: the fixed spelling appears on the thread but NOT in the card's claim comment, which is the carrier the enqueue gate's content limb reads."The cause is mine. That script defines a claim comment as one whose body carries a LINE BEGINNING
Claim:(:751-752). I had been opening my claims with**Claim:**— a line that begins with**, so no comment on this card was a claim comment at all by the predicate, however complete the reasoning under it.Measured across the cards this seat claimed tonight:
claim shape cards gate predicate bare Claim:lineobjectui#8735, objectui#8784 ✅ legible — both passed --pairat exit 0**Claim:**objectui#9026, objectui#9034, objectui#8832, objectui#8924, objectui#8811 ⛔ illegible ⭐ On objectui#9034 it was worse and worth naming: I also wrote the value in bold —
Clause-②: **yes**— so even a looseClause-②:\s*(yes|no)match failed. The declaration's spelling is fixed:Clause-②: yesorClause-②: no, exactly those two, unadorned, on their own line.The generalisable form
⛔ Markdown emphasis on a field a machine reads is not formatting, it is corruption. I added
**to make the claim easier for a human to scan and silently removed the card from a gate's population — the gate then reported not "this is wrong" but "there is nothing here to read", which is the same self-concealing shape objectui#9026 is itself about.⇒ ⭐ Nothing about the dispatch changes: the branch, the declaration, the dual-carrier state and every standing rule in the earlier claim comment are unchanged and still binding. A dev checking that the newest
Claim:names its branch will find this one, and it names the same branch.
Generated by Claude Code
os-dev-report
{ "issue": 9034, "status": "done", "branch": "claude/issue-9034-imported-defaults-describe", "pr": "https://github.com/objectstack-ai/objectui/pull/9086", "pr_number": 9086, "head_sha": "5131199465b75e1b2ebef107c07cbaa761d225a5", "premise_still_valid": true, "summary": "CONFIRMED, not refuted, and the defect is larger than the card states. The dispatch's prescribed route is REFUTED by measurement: in zod 4.4.3 a description is NOT def state - `.describe(d)` writes `{description:d}` into `z.globalRegistry`, a WeakMap keyed by the node, and the `description` getter reads back through `_zod.parent`, a link only zod's own `clone()` sets. So `cloneWithDef` (which builds `new Ctor({...def,...patch})` and sets no parent) does NOT carry a description either, and the file's `:167` docblock - the strongest fact in the dispatch - is half wrong: the clone rule saves `def.checks` and never saved the description. That makes the loss two-sited with one cause: the `default` arm (`.removeDefault()` returns the inner node, outer description discarded) AND every `cloneWithDef` rebuild. Fixed with ONE rule, `withDescriptionOf`, used by both. It uses `.describe()` and not a `_zod.parent` write for a measured reason: `.describe()` clones, and on the `.optional().default()` branch - taken 267 times on this surface - the replacement IS one of the spec's own objects, so a mutating carry would relabel `@objectstack/spec` for every workspace consumer. It carries description only: `globalRegistry.get()` also returns `id`, and re-registering an id rewrites the registry's id map. Also corrected the `:167` docblock rather than leaving a false sentence standing.", "population_measurement": { "instrument": "walks every schema-shaped export of every module subpath the spec publishes (subpath list READ from the spec's own package.json exports map, not hardcoded), runs the REAL stripImportedDefaults over each, and pairs the two graphs node-by-node. Not a replay of the two operations - the function itself.", "command": "MEASURE_OUT=/abs/path.json pnpm exec vitest run packages/types/src/__tests__/measure-9034.scratch.test.ts (scratch instrument, deleted after use; the same walk is now permanent in packages/types/src/__tests__/imported-defaults-describe-9034.test.ts, run by: pnpm exec vitest run packages/types/src/__tests__/imported-defaults-describe-9034.test.ts)", "spec_version": "17.4.0", "subpaths_declared_loaded": "17 / 17, 0 load failures (./openapi.json and ./package.json excluded as non-modules)", "schema_shaped_exports": 1635, "distinct_nodes_visited": 16120, "A_described_ZodDefault_nodes": 2024, "A_description_lost_BEFORE": 2024, "A_description_lost_AFTER": 0, "B_described_container_nodes_rebuilt": 1389, "B_description_lost_BEFORE": 1364, "B_description_lost_AFTER": 0, "card_comparable_narrow_scope": "top-level object-shape members only: 1256 object exports / 8314 members / 1234 ZodDefault members / 1031 described / 1031 LOST before / 0 lost after. The card filed 110 of 110; direction CONFIRMED, denominator larger because the card's instrument walked fewer exports. Nothing contradicts the card.", "descriptions_INVENTED_by_the_strip": "0 before, 0 after - the fix adds none where the source had none", "note_on_B_25": "25 of the 1389 read as 'kept' even before the fix. Investigated, not assumed: all 25 sit under a z.lazy whose getter builds a fresh graph per call, so the before/after pair was two independently constructed nodes. An artifact of the instrument's pairing, not a real survival." }, "removeDefault_verdict": { "question": "does .removeDefault() drop the description, or does zod propagate it?", "verdict": "CONFIRMED - it drops it. zod does NOT propagate.", "evidence": "z.string().default('x').describe('DESC'): .description === 'DESC'; .removeDefault().description === undefined; removed._zod.parent is unset. Root cause read from zod 4.4.3 source: describe() does `globalRegistry.add(this.clone(), {description})`, and registry.get() inherits only through `_zod.parent`.", "second_verdict": "The dispatch's route ('use the file's own cloneWithDef rule') does NOT work as stated and was refuted before being used: cloneWithDef(src,{}).description === undefined. cloneWithDef was FIXED to carry the description rather than relied upon to." }, "identity_property_pin": { "population_level": "reference-equal nodes across the same 16120-node surface: 9665 BEFORE, 9665 AFTER. The carry adds zero rebuilds.", "per_export": "every export with nothing to strip still comes back reference-equal; asserted over the whole surface, with the population split asserted non-empty on both sides so neither branch is vacuous.", "exceptions_pinned_by_shape": "two, and the assertion names the set rather than the count, so a NEW break cannot hide inside a matching number: (1) the walker's deliberate `lazy` exception; (2) rest-less tuples - PRE-EXISTING, filed as #9088, not fixed here. The carve-out itself is asserted NON-EMPTY so it goes red and must be deleted once #9088 lands." }, "controls": { "firing_control_ablation": "With packages/types/src/zod/imported-defaults.ts reverted to its origin/main content (f1190b0) and nothing else changed, the new pin goes 5 failed / 13 passed and the five are EXACTLY the description assertions. Mutation proven on disk before reading the result: withDescriptionOf occurrences 0, pre-fix default-arm line present 1, mutated blob hash aa76ce98 != HEAD blob 15a63179. Restored via `git checkout HEAD -- ABSPATH` under a trap; restoration proven by an EMPTY `git diff HEAD` (0 bytes) and on-disk hash == HEAD blob 15a63179 - not by an exit code. Post-restore confirmation run: 18/18 green. No build/dist step involved: the test resolves ../zod/imported-defaults.js to the package SOURCE, which the ablation's behaviour change itself demonstrates.", "absent_before_present_after": "ArtifactPackageEntrySchema.manifest.defaultDatasource carries 'Default datasource for all objects in this package'. Stripped member reads undefined BEFORE, the same string AFTER. Hand-built twin in the pin: control_positive before 'CTRL-D' / after undefined -> after fix 'CTRL-D'.", "identity_control": "a described default-free schema: reference-equal TRUE before and TRUE after, description 'CTRL-IDENTITY' intact both sides.", "negative_control": "same traversal for a node type that cannot exist returns 0 - reported only as PAIRED with the positive control above, which does fire. Two true negatives would not be a reading.", "tuple_finding_controls": "rest-less tuple identity FALSE, with-rest tuple TRUE, plain object TRUE - run against the origin/main walker, so the break is proven pre-existing and specific." }, "tests": "pnpm exec vitest run packages/types/ -> exit 0, 174 files / 3422 tests passed (baseline before the change: 173 files / 3404 passed; nothing pre-existing broke). pnpm exec vitest run packages/types/src/__tests__/imported-defaults-describe-9034.test.ts -> exit 0, 18/18. pnpm --filter @object-ui/types run type-check -> exit 0 (script is spelled type-check with a hyphen). pnpm exec eslint on both changed files -> exit 0. Caught and fixed a real repo rule on the way: object-ui/no-dynamic-import-in-test-hook rejected the census in a beforeAll; the census now builds at module scope. node scripts/check-changeset-presence.mjs -> exit 0 ('1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'). node scripts/check-changeset-no-major.mjs -> exit 0. check:control-bytes, check:spec-symbols, check:new-line-citations, check:esm-specifiers, check:self-import, check:unreferenced-sources, check:comment-mask-corpus -> exit 0 each. node scripts/check-governed-queue-guard.mjs --test -> NOT GOVERNED, 3 paths, none matched. grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' over all three changed files -> exit 1 (no control bytes). ABLATION: see controls.firing_control_ablation - 5 red without the fix, restoration proven by empty git diff HEAD and matching blob hash. All exit codes captured BEFORE any pipe (cmd > file 2>&1; EXIT=$?).", "ci": { "read_once_at_report_time_no_polling": true, "head_sha": "5131199465b75e1b2ebef107c07cbaa761d225a5", "total": 35, "success": 19, "skipped": 3, "non_green": 0, "in_progress": 13, "reported_total_count_equals_returned_array_length": true, "total_count": 35, "returned_array_length": 35, "in_progress_names": [ "Type Check", "Test (shard 1/4)", "Test (shard 2/4)", "Test (shard 3/4)", "Test (shard 4/4)", "Build Docs", "Lint", "Build & E2E", "Skill Example Check", "README Export Check", "Bundle Analysis", "Doc Snippet Type Check", "Live E2E (informational)" ] }, "mcp_calls": "1 - one targeted search_issues for dedup, after the REST search endpoint returned HTTP 403 (channel switch declared). Every other GitHub read and write went over repo-scoped REST.", "labels": { "pr_carrier": "needs:contract-review added to PR #9086 via the ADDITIVE endpoint (POST .../issues/9086/labels), then read back by a contrast read: labels on PR now ['needs:contract-review'], present TRUE. Matches the card's carrier as instructed.", "not_done": "did not flip ready, did not enqueue, did not enable auto-merge, never wrote the assignee field, posted no second Claim." }, "open_questions": [], "out_of_scope_findings": [ "filed as #9088: stripImportedDefaults rebuilds EVERY rest-less tuple, breaking the module's own identity property - zod spells 'no rest element' as def.rest === null and the tuple arm compares it against the undefined its own ternary produces, so null === undefined is false and the rebuild branch is always taken. Reproduced in two lines against the origin/main walker with controls firing the other way. Different defect class from description loss, and the repair moves reference identity of published mirror bindings (spec-subschema-parity asserts several with toBe), so it is its own contract-surface change. Labelled bug/package: types/finding/domain:spec; NOT given pm:queue - queueing is the PM's triage call.", "noted, not filed: the GitHub body mutator appends an attribution footer on EVERY write to a PR body, and the form differs by action - create appended the session-URL form, the later edit appended the bare form. PR #9086's stored body now carries three footer blocks. My own content survived byte-for-byte both times (verified by prefix comparison; zero angle-bracket-shaped fragments eaten). I stopped editing the body rather than loop. Successor: the PM seat, which writes and edits PR bodies every dispatch.", "noted, not filed: `eslint . --no-inline-config` over the whole repo reports 97 errors across 80 files, but the repo's actual lint script is `turbo run lint` -> per-package `eslint .` WITHOUT --no-inline-config, so those are inline-suppressed and are not CI failures. Flagging only because --no-inline-config is the spelling my operating rules name for a repo-level scan, and in THIS repo it reads far redder than CI does. Successor: none identified - no PR or person is heading for this file set." ] }
Generated by Claude Code
Seat correction — the fact I handed this dev as "⭐ the strongest fact, and it is not in the card" was itself half false. Measured and refuted by the dev before it was used.
My dispatch brief said:
This file already names description loss as the defect, and already has a rule against it —
cloneWithDef. Thelazyarm obeys that rule. Thedefaultarm at:201does not.and instructed: "preserve the outer node's description in the
defaultarm ⭐ using the file's owncloneWithDefrule rather than inventing a second mechanism."cloneWithDefnever carried a description. Measured on zod 4.4.3:cloneWithDef(src, {}).description === undefined. The mechanism is that.describe(d)writes{description: d}intoz.globalRegistry— a WeakMap keyed by the node — and thedescriptiongetter reads back through_zod.parent, a link that only zod's ownclone()sets.cloneWithDefbuildsnew Ctor({...def, ...patch})and sets no parent.⇒ the docblock at
:167is half wrong: the clone rule savesdef.checksand never saved the description. And the defect is therefore larger than the card states — two-sited with one cause, thedefaultarm and everycloneWithDefrebuild. The dev fixedcloneWithDefrather than relying on it, corrected the false sentence instead of leaving it standing, and used.describe()over a_zod.parentwrite for a measured reason: on the.optional().default()branch the replacement is one of the spec's own objects, so a mutating carry would relabel@objectstack/specfor every workspace consumer.⭐ The pattern, since this is the third refutation of mine tonight
what I asserted what refuted it objectui#8811's four tombstones exist and need pins the whole KanbanSchemainterface was deleted 13 hours earliera changeset "is published into packages/types/CHANGELOG.md"0 of 62 empty-frontmatter changesets reach a CHANGELOG hunk (control 40/40) cloneWithDefis "the file's own rule against description loss"cloneWithDef(src,{}).description === undefinedThe common thread is that I keep treating in-tree prose as evidence. A docblock, a card body, a triage comment — I read them, found them confident and specific, and passed them on as measurements. The
:167sentence was exactly the class of thing this lane files cards about (objectui#8924, objectui#8832, objectui#9026 are all "an assertion nobody re-checks"), and I quoted it as the strongest fact in a dispatch.⇒ ⛔ A claim written in the tree is a claim, not a reading — including one written by someone careful, including one that names a mechanism, including one I agree with. The rule I have been enforcing on devs applies to the briefs I write for them.
⭐ What made this land well anyway is that the brief said "⛔ Do not treat that framing as settled. Confirm or refute it yourself," and the dev did exactly that — refuting the prescribed route before using it rather than building on it and discovering the problem later.
On the population
The card filed 110 of 110; the dev measured 2024 described
ZodDefaultnodes losing their description across 16120 nodes and 1635 schema-shaped exports, plus 1364 container nodes — with the card-comparable narrow scope reproducing at 1031/1031. ⇒ direction confirmed, denominator larger because the card's instrument walked fewer exports. ⛔ Nothing contradicts the card.⚠️ The 25 B-nodes that read as "kept" before the fix were investigated, not assumed — all 25 sit under az.lazywhose getter builds a fresh graph per call, so the before/after pair was two independently constructed nodes. An instrument artifact, reported as such.Clause-②: yesstands, the carrier is on both sides, and a fresh ceiling-tier contract review is owed on the final head once CI is terminal.
Generated by Claude Code
- added a commit that references this issue
on Sep 17, 2026
stripImportedDefaultsdrops the.describe()of every imported default. Measured across the whole published protocol surface: 110 of 110 describedZodDefaultmembers lose their description, 0 survive.Provenance
Found by the contract reviewer of PR #9017 (card #8992) and declared out of scope for that PR. Filed here as its own card rather than ridden in — the PR's diff is three keys, this is the whole boundary. The numbers below are the seat's own re-measurement, not the reviewer's; they came out larger than the reviewer's PR-scoped "10 of 11".
The mechanism, from source
packages/types/src/zod/imported-defaults.ts, thecase 'default':arm of the walker (re-derive by content — anchors drift; it was at:200-202when this was written):.removeDefault()returns the node's inner type. The protocol's spelling is.describe(...)applied after.default(...), so the description lives on the outerZodDefaultnode — the node this line discards. Nothing carries it onto the replacement.Measurement — spec 17.4.0, whole surface
Script:
desc-loss-v3.mjs/desc-loss-split.mjs(seat scratchpad). It walks every schema-shaped export of every subpath export of the installed@objectstack/spectarball and replays the two operations the arm performs —.removeDefault(), thenz.optional(...)when the result is not already optional-in.ZodDefault.descriptionThe 18th subpath is
./openapi.json— not a module, excluded explicitly rather than swallowed by thecatch.Where the loss happens, split by step:
.removeDefault()ZodDefaultz.optional()re-wrapOne mechanism, not two. (The re-wrap would also lose a description, if any key were spelled
.describe().default()— none is. Probe:z.optional(z.string().describe('D')).description === undefined.)Instrument controls. Positive: a hand-built
z.object({ k: z.string().default('x').describe('D') })is detected by the same predicate (type === 'default'), reads"D"before andundefinedafter. Negative: the same traversal for a token that cannot exist returns 0. An earlier run of this instrument returned0 ZodDefault members— that was the instrument loading only the root export (14 schema-shaped exports, 38 members); the distribution print showed nodefaultnode anywhere, which is what sent me to the subpath exports. Recorded because a bare0from this script is not evidence without the export count beside it.Pull — measured, and it is currently ZERO
⛔ Do not dispatch this on the strength of the 110 alone. Nothing in objectui reads a zod node's
.descriptionin shipping code today:shape[...].description: 0. The three hits outside__tests__/areflow-canvas-seeds.spec-parse.test.tsx×2 andflow-node-config.spec-reconciliation.test.ts×1 — tests that merely live outside the__tests__/directory. The other 25 are retirement/tombstone tests reading objectui's ownretiredKey(...)guidance strings, which never pass through the strip.z.toJSONSchemaemitters bypass the boundary.packages/app-shell/src/views/metadata-admin/{view,page,package,dashboard,report}-schema.tsfeedSchemaForm— the surface where a JSON-Schemadescriptionbecomes the field's help text — but every one of them imports from@objectstack/spec/uior@objectstack/spec/kerneldirectly, not from@object-ui/types. They see the unstripped schemas, so they see the descriptions. This was the hypothesised consumer and it is refuted.So the defect is real and latent, not live. It bites the day any emitter, doc generator, or inspector is pointed at
@object-ui/typesinstead of at the protocol — at which point 110 keys silently lose their help text with no gate to catch it.Why it is still a defect, in the module's own words
The module's docblock states an identity property: a subtree with no
ZodDefaultin it comes back reference-equal, so the module "is exactly the identity function the day@objectstack/specadopts the same principle, with nothing to roll back." A subtree that does contain aZodDefaultcurrently comes back less faithful than it went in — it loses documentation the protocol declared. That is a departure from the property the module claims for itself, not a preference.It also matters for axis ③ (防 AI 写元数据 app 犯错): the descriptions are the protocol's own guidance to whoever authors a metadata document. Dropping them at the import boundary is a silent narrowing of what the boundary conveys, in the forbidden direction (narrower than the protocol).
Candidate fix, probed
One guarded line in the
defaultarm — carry the outer node's description onto the replacement:Probe on zod 4.4.3:
z.optional(src.removeDefault()).describe(src.description).description === "DESC"; the result still parsesundefined(true) and'a'(true) and carries no default (defOf(...).type !== 'default'). So the accept-set and the strip are both unchanged — only the description is preserved..describe()breaks the reference-equality half of the identity property for theunchanged(...)short-circuit above (a described node is a new node, but it was already a new node here), and whetherzod-mirror-authors-no-defaults-7735/imported-defaults-8317assert anything about the description of a stripped member.What was NOT measured
@object-ui/typesbuild — this replays the two operations against the protocol tarball rather than importing objectui's stripped bindings.Refs objectui#8992 · PR objectui#9017 (where the reviewer found it and correctly refused to ride it).
Generated by Claude Code