fix(cli): every os explain catalog example parses against its own schema - #16924
Conversation
…chema The catalog in `packages/cli/src/commands/explain.ts` is hand-maintained and does not derive from the spec, so its examples drifted behind the schemas they claim to demonstrate. The sweep landed for #14811 parses every entry's `example` against its real schema and pinned each failure as an `it.fails` xfail. This corrects the six entries that carried one, disposes of a seventh that had no type to check against at all, and promotes every xfail to a plain assertion — the ledger is now empty. - object select `options` are objects (`{ label, value }`), not strings - field the prose row that was the source of that error: `string[]` -> `SelectOption[]`, and the example demonstrates the real shape - view `ViewSchema` is the per-object CONTAINER; a single view's keys live inside a `list` / `form` / `listViews` / `formViews` slot - agent `tools` was removed in spec 17 (ADR-0064) with no key its value moves to — teach `skills`, `model` as an object, and the required `label` / `instructions` - app nav items need `id` plus the discriminant's own target key (`objectName` / `dashboardName`); `logo` / `defaultRoute` are not AppSchema keys - dashboard widgets bind a `dataset` and select `dimensions` / `values` (ADR-0021); `'chart'` is not a widget type; there is no dashboard-level `layout` - action `objectName`, `confirmText`, and `target` for a flow action; `"button"` is not in the type enum - trigger ADR-0088 retired the kind and its `TriggerSchema` never existed — the entry becomes an explicit redirect to `hook` and a `record_change` flow, the shape `workflow` already uses Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
📓 Docs Drift CheckThis PR changes 1 package(s): 74 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 71880ed93e76698c43e824d72a84a1cff2383cba && git checkout 71880ed93e76698c43e824d72a84a1cff2383cba
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 70f7d6d735505c03a80bdb279262af5aa7c77ff1 9aafd2b501fefb790c0406c83fb2b8607fe98b3f && git checkout -B drift-repro 70f7d6d735505c03a80bdb279262af5aa7c77ff1 && git merge --no-ff 9aafd2b501fefb790c0406c83fb2b8607fe98b3f
node scripts/docs-audit/affected-docs.mjs --json 70f7d6d735505c03a80bdb279262af5aa7c77ff1
|
PM review — seven cards, and every load-bearing claim I could check from source holds.
|
| # | claim | reading |
|---|---|---|
| 1 | the xfail ledger is empty | BOUND carries 9 entries and not one card: field — every bound entry is a plain assertion. The card?: number field survives as machinery, documented for "the next entry that arrives broken" |
| 2 | nothing is silently unswept | UNBOUND carries 2 (workflow, trigger), each with a test that ASSERTS its reason, plus a classifies every entry in SCHEMAS guard that reds when a new catalog entry appears unclassified |
| 3 | the guard is guarded | a separate harness-health test resolves every bound entry to a real schema and every example to an object — with its reason stated: "it.fails is green on ANY failure, so a broken subpath export or an unevaluable example would otherwise keep six xfails passing while measuring nothing at all" |
| 4 | #15176 is a redirect, not a rename | grounded — see below |
| 5 | #15170's prose half | explain.ts:74 now reads { name: 'options', type: 'SelectOption[]', description: '… Each option is an OBJECT — { label, value } … never a bare string.' } |
| 6 | file surface | 3 files, exactly as declared. No *.zod.ts, no exports map, no package.json, no generated surface |
⭐ On (1): my first grep for the promoted assertions returned 1 hit for their name, against a report claiming six. ⛔ I did not publish that. Read out, the assertions are generated in a for loop over BOUND with a template name — one literal, nine tests. An anchor finds sites; it does not judge them.
#15176 — I verified ADR-0088 §1 verbatim, and the disposition is right
docs/adr/0088-metadata-kind-admission-and-retirement.md, **Status**: Accepted (2026-07-05), §1:
triggerhad no stack collection, nodefineTrigger, no FS loader consuming**/*.trigger.ts, no executor — and its enum comment referenced aTriggerSchemathat never existed. … Authors: usehookfor sync data-layer logic, arecord_changeflow for async automation.
⇒ Redirecting executes a recorded decision; renaming would reverse one and needs an ADR, not a catalog edit. ⭐ The needs-user-decision escape hatch was available and correctly not taken.
triggers capability token in requires: — the FlowTrigger plugin family — is a different namespace and is unaffected.)" — because a redirect that reads as retiring the token too would be a new error. It was already handled: explain.ts:377 scopes the retirement to "the kind", and the example at :391 carries "The triggers capability token in a package's requires: is a DIFFERENT namespace and is unaffected by the retirement." Nothing changed for it, correctly.
One correction on this family is mine
The dispatch word and both triage comments attribute the agent.tools removal to ADR-0106, which is metadata-plane field-level security and unrelated. That error came through my dispatch; corrected on #15170 (5588724679). The real citations are ADR-0064 (Proposed, cloud-owned, part-superseded — the one the zod rejection quotes) and ADR-0109 (Accepted, implemented — the one the release notes lead with). ⭐ The seat's choice to carry the zod message's own number into the catalog stands: one number for the author who hits the rejection and then runs os explain agent. The citation-authority question is filed separately as #16927.
⭐ The out-of-scope finding is sharper than the seven this PR fixes
#16925, filed by the seat: os explain query teaches filters / sort, which QuerySchema silently drops — the example parses true and the parsed keys come back as only object, fields, limit. A copied query loses its filter and its ordering with no error, and the #14811 sweep is green on it by construction. The seven corrected here all fail loudly; that one does not. Correctly not folded in — closing that class needs a key-retention assertion, i.e. a new verification surface.
Status
CI at 16:56Z: 33 check runs, 0 duplicated names, 0 red, with only Lint & Repo Gates still in progress (started 16:41:47).
Generated by Claude Code
Accepted — CI is fully green on
|
| raw check-run rows | 33 |
| names carrying a superseded earlier row | 0 — every name appears exactly once, so the latest-per-name collapse is a no-op here |
| success | 30 |
| skipped | 3 (Console Pin Gate, Build Docs, Packed-tarball smoke (opt-in)) |
| failure / cancelled / timed_out | 0 |
| queued / in_progress | 0 |
⭐ Stating the collapse even where it changes nothing: on this head there is no duplicated name, so the raw list and the collapsed list are the same 33. On #16921 an hour ago they were 38 and 34, which is why the step is taken rather than assumed.
Landing precheck — all seven cards
- Clause ②:
node scripts/pm/check-clause2-carriers.mjs --pair 16924— exit 0, 7 ✓ / 0 ✗, re-run by me after the six pointer claims were repaired. Each of the six declarations was re-derived from that card's own delivered slice, not copied from [finding]os explain object's example does not parse as an Object —field.optionsis documented and sampled asstring[], the spec wants objects #15170's. Part-of PR must not also close its cardandNo other open PR may claim the same issue— both green, which is the check that matters on a PR carrying sevenFixeslines.Governed Surface Queue GuardandNo other open PR may claim the same single-writer path— green. Three files, none governed.- The substantive review is above and unchanged: the empty xfail ledger (9
BOUNDentries, zero carrying acard:), the twoUNBOUNDentries each with a test that asserts its reason, the completeness guard, the separately-asserted harness-health test, ADR-0088 §1 read verbatim for [finding]os explain triggerdocuments a metadata type the spec does not have — noTriggerSchemaexists, and the entry's sample is not a Hook either #15176, and thetriggerscapability-token exclusion carried in the delivered entry.
Two things recorded rather than fixed, restated so they survive the merge
- [finding]
os explain query's example teaches two keys QuerySchema SILENTLY DROPS —filters/sortarewhere/orderBy, and the #14811 sweep is green on it #16925 —os explain queryteachesfilters/sort, whichQuerySchemasilently drops. Sharper than any of the seven fixed here, because those all fail loudly and this one parses true. ⛔ Correctly out of scope: closing that class needs a key-retention assertion, i.e. a new verification surface. - [finding] the
agent.toolsretirement's author-facing citation is ADR-0064 — Proposed, cloud-owned and part-superseded — while ADR-0109 (Accepted, implemented) is what the release notes lead with #16927 — the citation-authority question behind theagent.toolsentry (the author-facing rejection cites ADR-0064, Proposed and cloud-owned, while ADR-0109, Accepted and implemented, is what the release notes lead with). ⭐ The delivering seat's choice to carry the zod message's own number into the catalog stands; the question is filed, not folded in. - The ADR-0106 misattribution in my dispatch word is corrected on [finding]
os explain object's example does not parse as an Object —field.optionsis documented and sampled asstring[], the spec wants objects #15170 (5588724679). That one was mine.
Arming
Ready-for-review first, auto-merge second — a draft conversion after arming destroys both. merge_method: merge whatever is requested; that field is inert here and the queue squashes. Queue membership will be read from the enqueued event, and enqueue can lag arming by around 90s.
⛔ I neither approve nor merge. On merge I will verify the landing on origin/main and strip pm:dispatched from all seven cards, reading each card's current labels first.
Generated by Claude Code
Fixes #15170
Fixes #15171
Fixes #15172
Fixes #15173
Fixes #15174
Fixes #15175
Fixes #15176
Clause-②: noRe-declared from the delivered diff, not inherited from the dispatch. The diff is three files —
packages/cli/src/commands/explain.ts,packages/cli/test/commands.test.ts, one changeset. It touches no*.zod.ts, noexportsmap, nopackage.json, no generated surface. Correcting an example so it parses against the schema the spec already declares narrows toward the declared contract: it relaxes no accept set and widens no published surface. Theos explaincatalog teaches a shape, it does not define one.The rebuttal condition the dispatch attached to #15176 does not fire: that entry's disposition redirects to two mechanisms that already exist (
hook, and a Flow oftype: 'record_change') and adds no authorable concept. It in fact deletes an entry's required and optional tables rather than adding any.The catalog is hand-maintained and does not derive from the spec, so its examples drifted behind the schemas they claim to demonstrate. The sweep landed for #14811 parses every entry's
exampleagainst its real schema and pinned each failure as anit.failsxfail naming a card. Below, each card keeps its own evidence: the before is that card's own zod rejection text, reproduced on this branch's base commit against the real schema.#15170 —
object:field.optionssampled asstring[]Before —
ObjectSchema(@objectstack/spec/data) rejects:After — parses.
SelectOptionSchemais a strict object of{ label, value }plus optionaldescription/color/default/visibleWhen:The prose-side half of the same defect — the sweep structurally cannot see it
The
fieldentry's optional-property table is not a parsed literal, so nothing reddens it, and it is where the example's error came from:Before —
{ name: 'options', type: 'string[]', description: 'Available choices (select fields)' }After —
{ name: 'options', type: 'SelectOption[]', description: 'Available choices (select/multiselect fields). Each option is an OBJECT — { label, value } plus optional description / color / default / visibleWhen — never a bare string. …' }The
fieldentry's own example, which parsed before and parses now, moves from a plain text field to a select field, so the real option shape is demonstrated where an author looks it up rather than only where it was broken.#15171 —
view: a flat literal where a CONTAINER goesBefore —
ViewSchema(@objectstack/spec/ui) rejects wholesale:This is not a drifted key name — the entry taught the wrong level, so fixing the literal alone would have left the tables describing a single view. Both tables now document the container (
name/label/object, plus the four view slots), and the example shows a single view's own keys inside a slot:After — parses:
#15172 —
agent:tools, a removed key with a migration notice attachedBefore —
AgentSchema(@objectstack/spec/ai) rejects with three issues:After — parses. Because the removal is not a rename, the replacement is authored rather than mapped:
The tables took the pass too, as the card asked.
labelandinstructionsmove to required, whereAgentSchemahas them. Three optional rows are gone:tools(removed),objects(measured absent fromAgentSchemaentirely), and a top-leveltemperature(the schema answers it withmodel settings live under model — write model: { temperature: … }).skills,surface,access,permissions,avatarandactiveare documented in their place.#15173 —
app: nav items rejected wholesaleBefore —
AppSchema(@objectstack/spec/ui) rejects with six issues, three per item:After — parses:
Bounded in-place fix, same entry, same defect class — the
navigationrow was only type-name deep, as the card says, but two neighbouring rows on this same table named keysAppSchemarejects. Measured on this branch:logoanddefaultRouteare dropped;brandingstays and its description says it carries the logo.labelalso moves to required, where the schema has it.#15174 —
dashboard: the pre-ADR-0021 inline analytics shapeBefore —
DashboardSchema(@objectstack/spec/ui) rejects with nine issues:As the card notes, this is the entry least suited to a mechanical fix:
'chart'is not a widget type at all, and the rewrite needs a real dataset to point at. The sample follows the shape the repo's own shipped dashboard uses (packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts).After — parses:
Bounded in-place fix, same entry, same defect class — the entry's optional table carried
{ name: 'layout', type: 'GridLayout', description: 'Widget positioning' }. Measured:The row is dropped for the per-widget
layout;columns,gapandglobalFilters(real keys) are documented;labelandwidgetsmove to required, where the schema has them.#15175 —
action: three keys, and one with no suggestionBefore —
ActionSchema(@objectstack/spec/ui) rejects:Two of the three are renames the spec itself names. The third is not, and that is the card's point:
flowis rejected with no suggestion, soos explain actionwas teaching atype: 'flow'action whose flow could not be named. Read fromui/action.zod.ts: a flow action names its flow intarget, whichTARGET_REQUIRED_TYPESmakes mandatory for every type exceptscript.After — parses:
The card notes the tables carry the same stale spellings, so they take the pass with the literal:
object/flow/url/confirmationbecomeobjectName/target/confirmText, andtargetis documented as required-except-script.Bounded in-place fix, same entry, same defect class — the required table advertised
type: '"button" | "url" | "flow" | "api"'.ActionTypeisz.enum(['script', 'url', 'modal', 'flow', 'api', 'form']):"button"is not a member (an author writing it getsinvalid_value), andscript/modal/formwere missing. The row now names the enum, and says a button is a location, not a kind.labelalso moves to required.#15176 —
trigger: an entry with no type to check againstThis one is different in kind, and it is not fixed the same way. Disposition chosen: the explicit redirect, mirroring the
workflowentry. Not a rename.Before —
TriggerSchemais absent from all four metadata-authoring subpaths (measured:'TriggerSchema' in specSurface === false), and the sample is not aHookeither:Why redirect, and why this was not the escape hatch. The dispatch's escape hatch fires if "rename vs redirect" is a product naming decision. It is not one here, because the decision has already been made and recorded.
MetadataTypeSchemainpackages/spec/src/kernel/metadata-plugin.zod.tscarries it inline:ADR-0088 §1 is Accepted (2026-07-05), retires the kind, states that "its enum comment referenced a
TriggerSchemathat never existed", and writes the author-facing prescription outright: Authors: usehookfor sync data-layer logic, arecord_changeflow for async automation. Under Prime Directive #13 that binds until a superseding ADR says otherwise, so choosing the redirect executes a recorded decision; choosing the rename would reverse one, which needs an ADR and not a catalog edit. It would also be the wrong shape twice over:hookis a real metadata kind in its own right, so renaming this entry to it would add a catalog entry for an authorable concept rather than pointing at existing ones — the exact condition the dispatch said to stop and re-declare on.After — the entry redirects, its
required/optionaltables are emptied likeworkflow's, and itsdocsPathmoves offautomation/trigger(no such page incontent/docs/) toautomation/hooks(which exists — the same control theworkflowentry'sautomation/workflowspasses):The entry's guard test changes shape with it. It still asserts
TriggerSchemais absent; it drops the "not a Hook either" leg (there is no literal left to rule out) and gains the two theworkflowentry uses — that the entry names itself a redirect, and that its example throws when evaluated, so "nothing was parsed here" is a property of the file. It also keeps aHookSchemaresolvability assertion, now defending the other half: that the redirect points at a mechanism that exists.The xfail promotion — the fence that makes this stick
it.failsis green on any failure, so a corrected entry left as an xfail would let the identical error return silently. All six were promoted to plainit(...)assertions in the same change; the ledger is now empty. The machinery stays, with a comment, so the next entry that arrives broken gets a card marker rather than a quiet skip.pnpm --filter @objectstack/cli exec vitest run test/commands.test.tsTests 25 passed | 6 expected fail (31)Tests 31 passed (31)xfail → assertion promotions: 6. Final pass count: 31 / 31, 0 xfail. Final run at
9aafd2b501.Ablation — the promoted assertion is load-bearing
One corrected entry (
object, #15170) reverted to the rejected shape, mutation and restore both proven by observing state rather than by reading an exit code. Verdict by test count on both legs.Testslinedde84bcebf531eb32acd1b334558634f221cd90d5285ee579205ceb51dbdd5eae85ec30a4b751bb9(differs); corrected-shape marker count 1 → 0, rejected-shape marker count 0 → 1Tests 1 failed | 30 passed (31)dde84bce…= HEAD blob; markers back to 1 / 0;git diff HEAD --name-onlyemptyTests 31 passed (31)The one failure is the promoted assertion itself, by name:
The script carries
trap 'restore' EXIT INT TERM(restore being its own shell function) with an absoluteREPO_ROOT-anchored path, restores withgit checkout HEAD -- PATH(never the bare form, which would take the mutation back out of the index), and refuses loudly on an empty or unchanged blob hash. No dist preflight applies: the sweep imports the subject relatively (../src/commands/explain), not through a packageexportsmap, so the mutated source is the resolution path — stated rather than assumed.Verification
pnpm --filter @objectstack/cli exec vitest run test/commands.test.ts→Tests 31 passed (31), exit 0.pnpm --filter @objectstack/cli typecheck→ exit 0 (tsc --noEmit+check:test-typecheck; the test layer compiles undertsconfig.test.jsonwith its debt ledger unchanged).packages/cliunit layer only. The diff touches no integration-layer file, no spawn entry point (bin/,test/helpers/serve-process.ts) and no driver/kernel boot path, so the integration layer is declared to CI rather than run here.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(2 paths at first, re-derived after the changeset landed: 49 → 58 families). All 58 run, each exit code captured before any pipe. Reconciled:58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN.PREREQUISITE NOT MET/NOTHING was measuredrather than a finding —check:dual-build-cjs-loads(exit 3),check:i18n(3),check:i18n-coverage(3),check:i18n-walk-parity(1). All four read built output that did not exist yet. Afterpnpm --filter @objectstack/cli buildall four are green, includingcheck:dual-build-cjs-loads— 104 published require entry point(s) across 67 package(s) load. Reported as the re-measurement it is, not as four failures that were argued away.ESLint#isPathIgnoredover every tracked lintable file → 6377 linted, 0 ignored. ② This run's file count read from--format json: 3 files (the changeset.mdis warned as unconfigured, which is not a finding), 0 errors, 0 warnings on the two TypeScript files, at9aafd2b501. ③ Invariance:eslint.config.mjsnever enables type-aware linting for any file —parserOptions.project/projectServiceoccur 0 times, and the config's own header records the measured positive control — so a diff confined to these 3 files cannot move the verdict on any of the other 6374. The repo-widepnpm lintis CI's run.pnpm check:nul-bytesgreen, plus a hand scan of all three changed files with the gate's own class of pattern: no match.@objectstack/clipublishesdist, andexplain.tscompiles into it, so operator-visible output moves.check:empty-changeset,check:changeset-no-majorandcheck:changeset-gate-self-testsall green.Docs-drift reading — done by hand, not delegated to the zero
node scripts/docs-audit/affected-docs.mjs --json, from a worktree whosegit status --porcelainwas empty. The tool's owncomputedOnreportsdirty: false,diffBase 44c849c7d66977ed28814f15853d3e9f984275b7. It named 80 docs (not a zero) across 32 anchors — an anchor list, which finds sites and does not judge them, so every hit below was read before it was scored.Hand token sweep of
content/, each result paired with a control sharing the failing query's vocabulary:os explain/objectstack explainos validate= 133releases/v16.mdx), a historical note about #3244 andownership— accurate, unaffected. Read, not edited.options: ['…']on a selectoptions:= 84Field.select({ options: [strings] }), the factory's documented shorthand — executed to confirm: it normalises to[{label,value}]and the result parses asFieldSchema. Not drift.tools: [ … ]skills:= 7defineSkill(...), wheretoolsis the live key (ADR-0064). Notagent.tools.object:/dashboard:objectName:= 77groupBy:/aggregate: 'count'dataset:= 45measures[]entries, ObjectQL querygroupBy, one React prop. 1 observation, below.confirmation:/flow:confirmText= 39TriggerSchema/defineTrigger/*.trigger.tsHookSchema= 1TimeRelativeTriggerSchema,ConnectorTriggerSchema) or theplugin-spec.mdxpassage, which labels itself. Observation below.content/docs/listViews= 33Action.createwithtype: 'form'; an ASCII diagram), 1 explicitly-labelled conceptual snippet. Observation below.All six release-owned pages the tool listed were read and none is falsified —
releases/v17.mdxalready tells authors to "move anything declared inagent.tools[]ontoskills", which is what this change now teaches, so the catalog moves into agreement with the published notes. ⛔ No release-owned page edited.One measurement failed and is reported as failed, not as a clean bill: probing the three
docsPathURLs live (objectstack.dev/docs/…) returned000for all three including the control that should exist, so the egress path is dead from this container and the live-site reading is unavailable. ThedocsPathconclusion below rests on repo-side evidence only.验收备注 — out of scope, noted, not filed
os explain queryteaches two keys the runtime SILENTLY DROPS. Its example passes the sweep today, and that green is false.BaseQuerySchemais a plainz.object, not strict, and haswhere/orderBy— notfilters/sort. Measured on this branch: the entry's own example parsestrue, and the parsed output's keys areobject, fields, limit. The filter and the ordering are gone with no error anywhere, so an author who copies it gets unfiltered, unordered rows. This is a worse failure mode than the seven fixed here, which all fail loudly. Not fixed in this PR: it is a different entry, no card covers it, and the sweep would need a new key-retention assertion — a new verification surface, so the bounded in-place-fix test fails on its fourth condition. Deduplicated first over all 113domain:cli+findingcards, all states, full pagination to a short page, with a positive control.content/docs/protocol/kernel/plugin-spec.mdxstill presentsdefineTrigger()andsrc/triggers/**/*.trigger.ts, and ADR-0088 dropped that suffix fromOPS_FILE_SUFFIX_REGEX. Read in full before scoring: the passage labels itself "part of the proposed ergonomic authoring surface — there is no such helper today" and points readers at hooks, i.e. it already makes this PR's redirect. Whether "proposed" survives a retirement is a documentation-audit judgement, not a mechanical defect. Successor: the docs-accuracy-audit lane — the page is insidescripts/docs-audit/handwritten-docs.json.content/docs/concepts/metadata-driven.mdx:122showsconst taskForm = { type: 'form', object: 'task' }, a flat view literal of the [finding]os explain view's example teaches a flat view literal —ViewSchemais a CONTAINER (list/form/listViews/formViews) #15171 shape, under a comment that marks it "Conceptual". Same lane and same file set as the row above.content/blog/protocol-first-development.mdx:717carries a flat kanban view literal thatViewSchemawould reject.content/blog/is outside the docs-audit scope (0 of its 190 entries are blog pages), so no standing lane covers it, and the whole post is pre-spec illustrative prose rather than one drifted key.docsPathin this catalog exceptworkflow's points at a page that does not exist incontent/docs/and is not covered byapps/docs/redirects.mjs:data/object,data/field,ui/view,ui/action,ui/dashboard,ui/app,automation/flow,ai/agent,data/query. The real pages aredata-modeling/objects,ui/views, and so on. I moved onlytrigger's, because that entry's rewrite required a real target; the rest is a whole-catalog link question with its own review, and it fails the bounded in-place-fix test on condition ② (each target needs judgement, not a mechanical rename). The live-site confirmation is the failed measurement noted above; the repo-side reading has its control —workflow'sautomation/workflowsdoes exist.agent.toolsremoval to ADR-0106; ADR-0106 in this repo is metadata-plane FLS object schema masking, unrelated. The zod rejection an author actually receives cites ADR-0064, andreleases/v17.mdxcites ADR-0109. I carried the zod message's own citation into the catalog, because that is the text the author is reading when they need it. Recording the divergence; not chasing it.Not done here
⛔ No merge, no approval, no auto-merge, no draft flip. ⛔ No test skipped, disabled or quarantined. ⛔ No file under
content/docs/releases/edited.Generated by Claude Code