PtcRunner.Kernel.JSONSchema (PtcRunner v0.15.0)

Copy Markdown View Source

Internal compiler for the bounded capability JSON Schema profile.

The accepted profile is a strict subset of JSON Schema 2020-12 containing type, title, description, default, properties, required, additionalProperties, propertyNames, items, enum, const, minimum, maximum, minLength, maxLength, minItems, maxItems, maxProperties, and the single bounded sha256 string format. General type unions are unsupported, roots are objects, and a missing additionalProperties on an object is normalized to false. A non-root node may use one scalar type or the bounded nullable form [<non-null type>, "null"]; source order is canonicalized and other type unions remain unsupported.

$schema selects the schema dialect; absence means the MCP default (2020-12). Because the accepted profile is a common subset of the allowlisted dialects, a supported root $schema URI (2020-12, or draft-07 as a deliberate compatibility translation) is accepted and removed, while unknown, malformed, and nested dialect markers are rejected. Vendor x-… extension keys and the standard non-validating default annotation are discarded from every level as a deliberate client policy — mainstream MCP SDKs emit them by default. They do not reach normalized output, encodings, hashes, or runtime argument construction. All other unknown keywords remain rejected.

Each normalized schema is at most 64 KiB with maximum depth 16, 128 properties per object, and 256 enum members. Host construction compiles with compile/1. Request-scoped schemas compile with compile_bounded/3 in the same time- and heap-bounded worker used for runtime validation. Proven schema invalidity is distinct from compiler or validator timeout, heap exhaustion, crashes, and malformed worker results. Runtime validation delegates to the compiled JSV root. Input rejection may retain a small explanation containing only schema-declared paths, keywords, and bounds; submitted values and undeclared property names never enter that explanation. Validation and explanation projection run together in one bounded worker. valid?/2 remains a host-side predicate for construction, tests, and the MCP structured-result check inside the already-bounded provider process. Dispatcher output admission uses validate/5.

Rejection reports the first proven fault as a closed rule atom plus the segments locating it inside the submitted schema document. Every segment is a key or index the submitted document actually carries, so the location always resolves in the file the author opens. The profile is deliberately closed, and its edges are not guessable from a bare refusal: an unsupported keyword, a misspelled type, and an unsatisfiable bound are three different authoring mistakes and must not report identically.

Summary

Functions

Compiles a request-scoped schema in a caller-cancelled bounded worker.

Returns every rule a rejection can carry, most specific first.

Validates a value and returns only bounded, schema-authored rejection facts.

Types

compiled()

@type compiled() :: JSV.Root.t()

rejection()

@type rejection() :: %{rule: atom(), segments: [segment()]}

segment()

@type segment() :: {:property, binary()} | {:index, non_neg_integer()}

violation()

@type violation() :: %{
  :argument => binary(),
  :constraint => binary(),
  optional(:expected) => term()
}

Functions

compile(schema)

@spec compile(term()) ::
  {:ok, map(), compiled()} | {:error, {:invalid_schema, rejection()}}

compile_bounded(schema, timeout_ms, max_heap_words)

@spec compile_bounded(term(), pos_integer(), pos_integer()) ::
  {:ok, map(), compiled()}
  | {:error, {:invalid_schema, rejection()}}
  | {:unavailable, atom()}

Compiles a request-scoped schema in a caller-cancelled bounded worker.

Proven profile rejections stay {:error, {:invalid_schema, rejection}}. Worker timeout, heap exhaustion, cancellation, crashes, and malformed worker results are {:unavailable, cause} and must not be treated as an invalid document.

rules()

@spec rules() :: [atom()]

Returns every rule a rejection can carry, most specific first.

The set is closed so a diagnostic boundary can render each rule from a fixed literal instead of forwarding compiler prose.

valid?(root, value)

@spec valid?(compiled(), term()) :: boolean()

validate(root, schema, value, timeout_ms, max_heap_words)

@spec validate(compiled(), map(), term(), pos_integer(), pos_integer()) ::
  :ok | {:invalid, [violation()]} | {:unavailable, atom()}

Validates a value and returns only bounded, schema-authored rejection facts.