# Agent Note: Unified JSON-value schema DSL

Status: implemented

English | [中文](2026-07-20-unified-json-value-schema-dsl.zh.md)

## Problem

Tool parameters used a small author DSL while subagent/workflow structured output used a separate raw JSON Schema subset and validator. The two vocabularies disagreed about roots, scalar constraints, and validation, so a typed canonical tool-output contract would either duplicate both paths again or accept schemas that some projection could not enforce.

## Decision

`dsh-tools` owns one JSON-value schema vocabulary with two representations. `ValueSchemaSpec` is the author form for any JSON root; `ParameterSchemaSpec` is its implicit object-property-map form with per-property `required: true`. `JsonSchemaNode` is the raw wire form. Both support string, finite number, integer, boolean, null, array, object, type-correct scalar `enum`/`const`, and exact-one `oneOf`; `{ type: 'json' }` is author-only sugar for an annotation-only unconstrained raw node.

An explicit author object must declare `additionalProperties: true | false`. The implicit parameter root and raw JSON Schema preserve the standard open default. `InferValue<S>` and `InferArgs<P>` derive TypeScript values from the same declarations that `valueSchemaSpecToJsonSchema()` and `parameterSchemaSpecToJsonSchema()` compile. `assertSupportedJsonSchema()` rejects unsupported or misplaced keywords, and `validateJsonSchemaValue()` enforces the accepted subset against the lossless `JsonValue` boundary: no `undefined`, negative zero, non-finite numbers, sparse arrays, cycles, exotic objects, functions, symbols, or other coercive values. Intrinsic plain Object and Array containers remain plain across JavaScript realms; subclasses remain exotic. Validation and snapshot traversal are iterative, so valid nesting is limited by available memory rather than the JavaScript call stack.

Object-rooting is a consumer rule rather than a vocabulary restriction. Subagent and workflow caller-defined structured outputs use `assertObjectJsonSchema()` and `ObjectJsonSchema`; tool outputs may use any root. Dynamic Cordis registrations rebuild realm-foreign schemas into host-owned JSON, preserve raw-wrapper openness, and require direct-DSL object openness before calling the same compiler.

## Alternatives considered

- **Keep separate parameter and structured-output schema systems:** rejected because every added output construct would require parallel inference, compilation, validation, and code-generation changes with no useful ownership boundary.
- **Adopt full JSON Schema or Ajv:** rejected because the harness must fail on every construct it cannot project into its generated SDK and validators; accepting a larger language would make enforcement and model guidance dishonest.
- **Make every object implicitly open or closed:** rejected because either choice hides a consequential author decision. Only the legacy-shaped implicit parameter root and external raw schema retain an intentional default.
- **Define `oneOf` as first-match:** rejected because branch ordering would change validation semantics and allow overlapping branches to hide ambiguous values.

## Consequences

- Parameter validation, output validation, schema-to-TypeScript generation, subagent/workflow guards, and dynamic registration share one enforced vocabulary.
- Output declarations can infer object, array, scalar, or null roots; subagent/workflow structured outputs remain object-rooted at their existing seams.
- Explicit object openness and type-correct literal constraints make malformed declarations fail during authoring or registration rather than during a later model call.
- Raw tools may still register broader JSON Schema directly, but unified code generation treats unsupported schemas as unknown instead of pretending to enforce them.
- Runtime and compile-time tests cover every root, exact-one overlap/no-match behavior, raw open defaults, explicit openness, lossy JSON values, and inference.
