import { FieldSchema, KindSchema } from './core.js'; /** * OpenAI/provider JSON Schema → KindSchema converter + __kind injection. * * The strategic keystone: `buildAgentSchemaWithRenderBlockSupport` turns "a * user/agent defined an output schema" into "the platform has a render * contract for it" by injecting the `__kind` discriminator into the agent's * output schema (root + array items), so the model emits self-identifying * objects from then on. * * Moved from app/(dev)/demos/json-block-detector/schema-converter.ts. */ type JsonSchemaNode = Record; type ConversionProblem = { severity: "error" | "warning" | "info"; path: string; message: string; }; type DroppedMetadata = { path: string; dropped: Record; }; type FieldComparison = { field: string; aiPresent: boolean; blockPresent: boolean; aiSummary: string | null; blockSummary: string | null; status: "match" | "ai_only" | "block_only" | "type_mismatch" | "ai_richer" | "block_richer"; detail?: string; }; /** One standalone flexible_data block schema row. */ type BlockSchemaDraft = { slug: string; label: string; fields: Record; /** * NON-OBJECT ROOT form (KindSchema.root): the kind's value is this field's * shape at the top level (scalar / array / json / OPEN object). When set, * `fields` is empty — mirror of `KindSchema.root`. */ root?: FieldSchema; }; type ArrayItemKindBinding = { arrayField: string; itemKindSlug: string; }; type SchemaConversionResult = { schemaName: string | null; strict: boolean | null; /** Every block schema row required for the converted shape (1..n). */ blockSchemas: BlockSchemaDraft[]; /** OPTION 2 — same input shape with __kind injected for agent output. */ agentSchemaWithKinds: unknown | null; problems: ConversionProblem[]; droppedMetadata: DroppedMetadata[]; comparisons: FieldComparison[]; }; type SavePlanEntry = { draft: BlockSchemaDraft; existsInDb: boolean; willSave: boolean; }; type ItemKindRefCheck = { parentSlug: string; field: string; itemKind: string; satisfied: boolean; source: "batch" | "database" | "missing"; }; type SavePlanValidation = { entries: SavePlanEntry[]; itemKindRefs: ItemKindRefCheck[]; newCount: number; canSave: boolean; errors: string[]; }; /** * Inject the `__kind` discriminator into one object schema (const when * strict, enum otherwise; `__kind` prepended to required; strict also pins * additionalProperties:false). Shared with the REVERSE converter * (kind-to-json-schema.ts) so both directions stamp identical discriminators. */ declare function injectKindIntoObjectSchema(objectSchema: JsonSchemaNode, kindSlug: string, strict: boolean): JsonSchemaNode; declare function buildAgentSchemaWithRenderBlockSupport(input: unknown, rootKindSlug: string, arrayBindings: ArrayItemKindBinding[], strict: boolean): unknown | null; declare function normalizeAiSchemaInput(input: unknown): { name: string | null; strict: boolean | null; rootSchema: JsonSchemaNode | null; parseErrors: string[]; }; type ConversionCore = { schemaName: string | null; strict: boolean | null; blockSchemas: BlockSchemaDraft[]; problems: ConversionProblem[]; droppedMetadata: DroppedMetadata[]; }; declare function convertAiSchemaToBlockFields(schemaName: string, rootSchema: JsonSchemaNode, strict: boolean): ConversionCore; declare function compareWithExistingKindSchema(convertedFields: Record, existing: KindSchema | null): FieldComparison[]; declare function runSchemaConversion(input: unknown, existingSchemas: Record): SchemaConversionResult & { parseErrors: string[]; }; declare function validateBlockSchemaSavePlan(blockSchemas: BlockSchemaDraft[], existingSlugs: string[], hasConversionErrors: boolean): SavePlanValidation; declare function fieldsToDbPayload(fields: Record): Record; declare function isDuplicateBlockSlug(slug: string, entries: Array<{ slug: string; }>): boolean; /** * KindSchema → provider-ready JSON Schema — the REVERSE of the OpenAI * converter in openai-schema-converter.ts. * * The requested root kind renders INLINE as the top-level object; every * OTHER kind it transitively references (via `{type:"object", kind}` and * `{type:"array", itemKinds}` fields, including inside inline_objects) * becomes a `$defs` entry referenced with `$ref: "#/$defs/"`. * References back to the ROOT kind use `"#"` — the standard JSON Schema * recursive-root reference (OpenAI structured outputs support both forms). * Collection is cycle-safe: each kind enters the closure exactly once. * * Round-trip contract — output fed through `runSchemaConversion` * (openai-schema-converter.ts) reproduces the source KindSchemas for the * root and every array-item child (enforced by * __tests__/kind-to-json-schema.test.ts). The 2026-07-15 expressivity * extension closed the three historical asymmetries: `record` now has a * forward target (properties-less objects → record scalar/json), multi- * itemKind arrays read back through items-anyOf when each variant declares * its `__kind`, and nullable anyOf preserves the member list + nullable flag * instead of flattening to `string`. Remaining accepted narrowings: * - multi-kind array items emitted as $refs (not inline __kind objects) * still cannot be read back — the forward converter rejects $ref. * - a nullable object-REF (`anyOf: [$ref, {type:"null"}]`) degrades for the * same $ref reason. * - JSON Schema `integer` narrows to `number` (the map_result precedent). */ type KindJsonSchemaOptions = { /** * Provider strict mode: additionalProperties:false on every kind object * and inline object, and `const` (vs `enum`) __kind discriminators — * mirrors makeKindJsonSchemaProperty / injectKindIntoObjectSchema. */ strict?: boolean; /** * Inject the `__kind` discriminator (+ required) into the root and every * $defs kind object. Default true — this is what makes the emitted * objects self-identifying for the render pipeline. */ injectKind?: boolean; }; type KindJsonSchemaExport = { /** The root kind slug — doubles as the provider schema name. */ name: string; schema: JsonSchemaNode; strict: boolean; /** * Referenced kinds the resolver could not supply. Each still gets a * permissive `$defs` object stub (never additionalProperties:false, even * in strict mode — a __kind-only strict object would reject every real * payload) so the export stays structurally valid. Surface these loudly. */ unresolved: string[]; }; /** * Every kind referenced by a field map — `{type:"object", kind}` refs and * `{type:"array", itemKinds}` members, recursing through inline_objects. * First-sighting order, deduplicated. Shared read helper (the registry * catalog builds its uses / used-by graph from this). */ declare function collectReferencedKinds(fields: Record): string[]; /** * Schema-level referenced kinds — field-map refs PLUS the refs a non-object * root form carries. Use this (not `collectReferencedKinds(schema.fields)`) * whenever the schema in hand may be root-form. */ declare function collectSchemaReferencedKinds(schema: KindSchema): string[]; declare function kindSchemaToJsonSchema(kind: string, resolve: (kind: string) => KindSchema | undefined, options?: KindJsonSchemaOptions): KindJsonSchemaExport | null; /** * JSON Schema → KindSchema — THE registry-facing direction. * * 🚨 WHY THIS FILE IS THE POINT OF THE WHOLE CONVERT LAYER (Arman's ruling, * 2026-08-29): a kind's fields must be DERIVED from its schema, never stored a * second time. `content_ir.kind_definition` used to carry both — a JSON Schema * AND a flattened field list written beside it — and the two had already * diverged on 13 of the 62 live kinds that carried both: stored lists holding * `__kind` as if it were a data field, enums flattened to bare strings, * min/max bounds and required flags the schema states and the copy never * learned. A cache that can disagree with its source is not a cache. * * The heavy lifting lives in `openai-schema-converter.ts` (it also serves the * agent-authoring flow, which needs the problems/dropped-metadata detail). * This module is the narrow, honest door the registry calls: one schema in, * one `KindSchema` out, plus whatever child kinds the schema declared inline * or through `$defs`. * * Accepts both storage shapes seen in the wild: a bare JSON Schema, and the * provider `{name, schema, strict}` response-format envelope that 5 live rows * carry. */ type KindSchemaFromJsonResult = { /** * The requested kind's schema, or null when the input could not be read as * an object contract at all. NEVER a partial guess: a field the vocabulary * cannot express degrades to `json` (widening, always safe), and only a * structurally unusable root yields null. */ schema: KindSchema | null; /** * Child kinds the schema declared inline or via `$defs`, keyed by slug. * A registry that ingests these gets the whole tree from ONE row's schema — * which is how a nested kind renders before its children's rows load. */ children: Record; /** Conversion notes. `severity: "error"` entries are worth screaming about. */ problems: ConversionProblem[]; }; type KindSchemaFromJsonOptions = { /** * The registry row's `metadata.disposition`. A `record` kind's schema gains the four * control keys (`_replaces`, `_new`, `_record_id`, `_records`) as optional render fields * (`withControlKeys`, KINDS-GLUE wave 2 §1.4), so an agent's patch, batch or signal draws * through its kind instead of reading as unknown keys. Any other value adds nothing. */ disposition?: string | null; }; /** * Derive `kind`'s field schema from its JSON Schema. * * Pure and synchronous — no registry, no IO — so a host can call it from a * warm load, a cold fetch, a reducer, or a test. */ declare function kindSchemaFromJsonSchema(kind: string, jsonSchema: unknown, options?: KindSchemaFromJsonOptions): KindSchemaFromJsonResult; /** * The four control keys an agent may put beside a kind's values — the TypeScript twin of * `matrx_graph.kind_control_keys.with_control_keys` (aidream). ONE definition, two languages: * for the same input both produce the same JSON (guard W2-G7, the shared golden * `packages/matrx-graph/tests/fixtures/kind_control_keys_golden.json`). * * `_replaces` ("this replaces that output"), `_new` ("this is something new"), `_record_id` * ("change this record") and `_records` ("several records") are never part of a kind and no * registry row carries them — they are added where a schema is USED, for `record` kinds and * table kinds only: * * - `withControlKeys(schema, { mode: "stored" })` — the JSON Schema a value is validated * against: all four declared optional (nullable), and the root's required Fields lifted when * the value is a patch (`_record_id`) or a batch (`_records`) — one branch APPENDED to `allOf`, * never replacing it. * - `withControlKeys(schema, { mode: "authored", many, allowEdit })` — what the model is shown: * `_replaces`/`_new` always, `_record_id` only with `allowEdit`, and with `many` the root is * the batch `{__kind, _records: [], ...}`. Signals are OPTIONAL — the * provider translators widen them to required-and-nullable on the wire and prune a null back. * - `withControlKeyFields(kindSchema)` — the render-level twin: the four keys as optional * render fields of a `KindSchema`, so a patch, a batch or a signal is drawn by its kind's * component instead of being reported as unknown keys. The web schema is never stricter * than the store. * * Design: common-docs `projects/data-doctrine-adoption/v6/KINDS-GLUE-WAVE2-DESIGN.md` §1.4. */ /** The four keys, in the order they are declared. */ declare const CONTROL_KEYS: readonly ["_replaces", "_new", "_record_id", "_records"]; type ControlKey = (typeof CONTROL_KEYS)[number]; /** The one disposition whose kinds take the control keys (table kinds are always `record`). */ declare const RECORD_DISPOSITION = "record"; type ControlKeysMode = "stored" | "authored"; type WithControlKeysOptions = { mode: ControlKeysMode; /** Authored mode: the root becomes the batch `{__kind, _records: [...]}`. */ many?: boolean; /** Authored mode: the model may name `_record_id` (the run may edit its own outputs). */ allowEdit?: boolean; }; /** Whether a kind with this disposition takes the control keys. */ declare function controlKeysApplyTo(disposition: string | null | undefined): boolean; /** * A copy of `schema` that carries the control keys for `mode`. A schema that is not an object * with `properties` (a root-form kind, the "any value" `{}`) is returned unchanged. */ declare function withControlKeys(schema: T, options: WithControlKeysOptions): T; /** The render fields the control keys add to a `KindSchema` — all optional, none stricter than the store. */ declare const CONTROL_KEY_FIELDS: Readonly>; /** * `schema` with the four control keys as optional render fields. A root-form kind (no field * map) is returned unchanged. Keys the kind itself declares are left as the kind declares them. */ declare function withControlKeyFields(schema: KindSchema): KindSchema; /** Split a value into (values without the control keys, the control keys present). */ declare function stripControlKeys(value: Record): { values: Record; signals: Partial>; }; export { CONTROL_KEYS, CONTROL_KEY_FIELDS, RECORD_DISPOSITION, buildAgentSchemaWithRenderBlockSupport, collectReferencedKinds, collectSchemaReferencedKinds, compareWithExistingKindSchema, controlKeysApplyTo, convertAiSchemaToBlockFields, fieldsToDbPayload, injectKindIntoObjectSchema, isDuplicateBlockSlug, kindSchemaFromJsonSchema, kindSchemaToJsonSchema, normalizeAiSchemaInput, runSchemaConversion, stripControlKeys, validateBlockSchemaSavePlan, withControlKeyFields, withControlKeys }; export type { ArrayItemKindBinding, BlockSchemaDraft, ControlKey, ControlKeysMode, ConversionProblem, DroppedMetadata, FieldComparison, ItemKindRefCheck, JsonSchemaNode, KindJsonSchemaExport, KindJsonSchemaOptions, KindSchemaFromJsonOptions, KindSchemaFromJsonResult, SavePlanEntry, SavePlanValidation, SchemaConversionResult, WithControlKeysOptions };