Skip to content

[finding] the published JSON Schema is WIDER than the zod schema it is generated from wherever a .refine() carries the rule — an author validating against packages/spec/json-schema/** gets a green for metadata the runtime refuses #18670

Description

@os-litant

Measured by the os-dev patch round on #15811 (report 5715173579) and handed to the seat to file — devs do not POST /issues. ⛔ Not introduced by that PR (it arrived with the refine that round added, but the projection gap is general and pre-existing).

Measured

z.toJSONSchema in zod 4.4.3 returns byte-identical output for a plain record, the same record with a .refine(), and the same record with an aborting .refine(). ⇒ a refinement cannot reach the published JSON Schema at all.

Worked instance, packages/spec/json-schema/system/TraceSamplingConfig.json:

condition.anyOf[0] = {"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}

That accepts {dialect:'cel'} — which the runtime refuses. The string branch likewise carries no non-blank constraint, while the runtime requires non-empty-after-trim.

⭐ Why this is class (c) and not a generator nit

This is the metadata-authoring trap in its most exact form. packages/spec/json-schema/** is what an author — or an AI — validates against, and what the reference tables are generated from. Today it says yes to shapes the runtime says no to.

⇒ The failure is silent at the moment of authoring and loud only later, which is the inversion this project exists to prevent: 「声明即强制」 is violated in the direction where the published declaration is more permissive than what runs.

⚠️ And it is general: it applies to every refinement in packages/spec, not to the one slot that surfaced it. The tracing slot is the specimen, ⛔ not the population. Whoever takes this must census the refinements first.

What it is NOT

⛔ Not an argument to remove refinements — the runtime behaviour is correct and is what the ADR-0049 direction wants. The gap is that the projection drops it.
⛔ Not fixable by hand-editing the generated page: the #15811 round measured that regenerating alone can never move the type cell, and hand-edits are reverted by the next generation and red the generated-artifact gate. That round worked around it at the slot's .describe(), which documents the rule in prose — a mitigation, ⛔ not a fix, and it does not help a machine validator.

Direction for a fix

Either the projection learns to emit what a refinement constrains (where the refinement is expressible in JSON Schema — propertyNames, not, minLength and friends cover a lot of them), or the published artifact declares that it is a floor rather than the contract, loudly enough that a validator author cannot miss it. ⚠️ The second is weaker and should not be chosen just because it is cheaper — a schema that is documented to be wrong is still wrong to a machine.

Carrier: whoever owns packages/spec/scripts/build-schemas.ts's projection.

Dedupe words: json-schema projection refine dropped, toJSONSchema byte-identical refined, published schema wider than zod, TraceSamplingConfig condition anyOf record, authorable surface validator green runtime refuses.

os-decision-facets

⚠️ Added by the domain:spec seat when the half-state patrol row H62 measured this body as carrying no marker in either spelling. This seat filed the card, so this is its own filing duty discharged late — ⛔ nothing already on the face was removed or reworded. Item 1 has since LANDED (PR #18729, squash a49e8ae963); the facets below are about item 2 only, which is what remains in the box.

  • ① 项目长远合理性 — the published packages/spec/json-schema/** is measurably WIDER than the zod it is generated from wherever a .refine() carries the rule. Item 1 stopped the silence: every affected artefact now carries an x-dropped-refinements annotation and a ratchet-only ledger. ⭐ That annotation IS item 2’s worklist — every repaired site deletes a ledger row, so the migration is measurable from its first commit to its last. Item 2 decides whether the FILE stops lying or only keeps confessing.
  • ② 实际业务拉动 — an author validating against the published JSON Schema gets a GREEN for metadata the runtime then refuses. Census on the tree, reproducible: 126 refinement sites (refine 48 + superRefine 75 + check 3), 243 published schemas each dropping at least one, 737 dropped sites, and 0 sites actually projected into a published file. Mode-invariance proven (737/243 both with and without OS_EAGER_SCHEMAS=1), so the numbers are not an artefact of which vitest project ran the generator.
  • ③ 防 AI 犯错 — the failure shape is a machine validator answering PASS on a document the runtime rejects: right at the door, wrong at run time, green throughout. ⚠️ The card’s own text warns against choosing the cheap option BECAUSE it is cheap.
  • ④ 创业阶段不扩散 — A narrows the published shapes and is therefore breaking for already-passing documents ⇒ ADR-0087 disposition + a minor changeset under the launch-window convention; the implementing round recommended it, with a second census first classifying all 737 sites as expressible vs not. B is zero cost and changes no contract, but leaves the validator green on metadata the runtime refuses. C narrows only where the refinement is total and mechanically derivable, which is a smaller blast radius than A but ships two classes of rule on one published surface with nothing on the file distinguishing them.

Prior rulings read: ADR-0087 (semantic migration entries — what A or C would owe) · ADR-0049 (enforce-or-remove, the sibling discipline) · the launch-window convention that a breaking change ships minor + BREAKING banner, ⛔ never major (check-changeset-no-major.mjs enforces it). ⛔ No prior ruling decides whether the published projection may narrow; that is what this card asks.

The question, in one line: does the JSON Schema projection start emitting what a .refine() constrains wherever that is expressible (A — breaking for documents that validated yesterday), keep only item 1’s annotation and leave the shapes alone (B — the machine validator stays wrong on purpose), or narrow ONLY where the refinement is total and mechanically derivable (C)? ⛔ This seat states the options and does not grade them: it is a public-contract change.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions