import { ComponentType } from 'react'; import { KindSchema, RecordValueType } from './core.js'; import { IrResidue, IrPath, CanonicalBlockIR } from './core.js'; /** * KindDefinition — one kind, many facets. THE canonical registry entry. * * This does NOT merge BlockComponentRegistry or artifact-type-registry; the * `legacyBlockType` and `artifact` facets are FACADES pointing into them, so * migration is incremental and each registry keeps its own job. */ /** * The uniform props contract for kind-driven block components. This is what * de-special-cases flashcards: no component receives bespoke glue — every * kind component gets exactly this shape. */ interface KindBlockProps { kind: string; schema: KindSchema; /** Compliant snapshot value (schema fields + __kind). */ data: Record; status: "streaming" | "complete" | "error"; residue: IrResidue | null; path: IrPath; /** ParseSession identity for live child subscriptions; null after reload. */ identity: string | null; /** Child-kind schema lookup (replaces passing allSchemas around). */ resolve: (kind: string) => KindSchema | undefined; } type KindTier = "eager" | "warm" | "cold"; /** * A TABLE AS A KIND (KINDS-GLUE wave 3 §1.4): what the host learned from the store's * `custom.table_kind_facts` when it answered a `table:` kind. `stamp` moves on any Field or * choice change; `optionsTableIds` are the Tables whose records are this Table's choices (a change * there is a change here). `refusal` is the store's own sentence when the person may not open the * Table — the definition still exists so the card can say so instead of loading forever. */ interface TableKindFacet { tableId: string; organizationId: string | null; name: string | null; titleField: string | null; stamp: string | null; fieldIds: string[]; optionsTableIds: string[]; refusal?: string | null; } interface KindDefinition { /** Canonical slug — THE key. */ kind: string; /** null until the warm/cold fetch delivers it. */ schema: KindSchema | null; /** * Where the schema came from. `"table"` (KINDS-GLUE wave 3): the kind is `table:` — "a * record of that Table" — and its schema is derived from the Table's live Fields * (`@ai-matrx/records` `tableRenderSchema`), never a `kind_definition` row. */ schemaSource: "system" | "content_ir" | "table"; tier: KindTier; /** Present only for `schemaSource: "table"` — the Table the kind is derived from. */ table?: TableKindFacet; /** Component facet — lazy-loaded renderer for this kind. */ component?: { load: () => Promise<{ default: ComponentType; }>; }; /** Facade → BlockComponentRegistry type string (e.g. "flashcards"). */ legacyBlockType?: string; /** * Legacy-bridge facet: derive the existing component's `serverData` from a * canonical envelope. This is what lets a kind light up the REAL component * (FlashcardsBlock, …) with zero component changes during migration. */ toLegacyServerData?: (envelope: CanonicalBlockIR) => Record | undefined; /** Facade → artifact-type-registry canvasType. */ artifact?: { canvasType: string; }; /** * Markdown export facet — the FORWARD leg of artifact ⇄ markdown. Receives * the ZERO-LOSS reconstructed value object (the CALLER reconstructs — for * persisted structured artifacts `content.data` already IS that object; * this facet only renders it; `__kind` discriminators may still be * present and must be ignored). MUST produce human-readable markdown * (headings / lists / bold), never a JSON dump — a fenced json body under * a heading is acceptable only for inherently-code payloads (e.g. a * schema_proposal's JSON Schema). Unknown extra keys the renderer doesn't * understand MUST be appended under a small "Additional details" * (key: value) section so nothing silently vanishes. Kinds without this * facet fall back to `genericKindMarkdown` (kinds/kind-markdown-utils.ts). */ toMarkdown?: (value: Record) => string; persistence?: { persistStructured: boolean; }; /** Future XML tags / kind aliases resolving to this kind. */ discriminatorAliases?: string[]; /** * PARTIAL-READY opt-in — the streaming partial-kinds posture. * * While a structured region streams, the server may announce a PROVISIONAL * instance of this kind on `metadata.__ir_partial` (valid, closed JSON that * may be missing required fields). The default posture is PROGRESSIVE * (Arman, 2026-10-14): the provisional value is routed to the SAME component * that renders the final one, filling in as tokens arrive — exactly what a * chat stream already does. * * Set this to `false` only for a kind whose component is known to break on a * half value: its block keeps its loading skeleton until complete. `true` is * the default spelled out. A kind * with a `toLegacyServerData` bridge must ALSO pass `{ provisional: true }` * to `makeCompleteEnvelopeBridge` (pinned by a test). A component that * throws is caught, screams, and drops this kind back to withhold for the * session. * * Contract: common-docs/systems/architecture/content-ir/FEATURE.md */ partialReady?: boolean; /** * Loading-library slug (`kind_definition.metadata.loading_component`) — * which of the ~20 hardcoded loading components renders while this kind's * instance streams in / its schema+component fetch is in flight. Null or * unknown slugs fall back to the generic structured skeleton. */ loadingComponent?: string | null; } /** * Kind ↔ content_ir storage shape — the pure, DB-free reshaping between the * consumed `KindSchema` and the `content_ir.kind_definition.data` array + * `content_ir.kind_edge` rows. * * Two exact inverses: * - `kindSchemaToStorage` — the MIGRATION / write direction: a parsed * `KindSchema` (fields Record, targets inline) → an ORDERED `data` array * (targets stripped) + externalized `kind_edge` specs (targets, keyed by * field PATH). This is what pours `flexible_data` (and future authored * kinds) into the canonical tables. * - `storageToKindSchema` — the ADAPTER / read direction: `data` array + * edges → the same `KindSchema` the parser/emitter consume, re-attaching * `object.kind` / `array.itemKinds` from the edges by path. * * Design invariants (from the 2026-07-05 kind-registry storage design, since deleted): * - The `data` element is `FieldSchema` + `name`, MINUS ref targets. Order is * intrinsic to the array (fixes the jsonb key-reorder bug). * - `kind_edge` is the SINGLE source of truth for kind→kind refs: `object` * → one edge (position null); `array` → N edges (union) ordered by * `position`. `field_name` is a dot-PATH so a ref nested inside an * `inline_object` still gets a distinct, collision-free edge. * - `inline_object` is structural: its `fields` is an ordered array, it never * becomes a registry row and never carries `__kind`. Its nested refs DO get * edges (path-prefixed) so cascade/pinning see every dependency. Its * `open` flag persists on the element (losing it is the open-empty-object * defect). * - `union.kinds` refs externalize to edges exactly like `array.itemKinds`; * the stored element keeps `hasKinds: true` so lost edges scream on read. * - A NON-OBJECT ROOT (`KindSchema.root`) stores as ONE reserved element * named `ROOT_STORAGE_NAME` ("__root") — the only element allowed in that * kind's `data`. Real fields may never use the reserved name. * * NO imports of supabase / the DB — this is pure and unit-tested by round-trip * (`__tests__/kind-storage-transform.test.ts`). */ /** * Reserved `data[]` element name for a NON-OBJECT ROOT form (`KindSchema.root`). * A root-form kind stores exactly one element under this name; the read * direction reconstructs `{ root }` instead of a field map. The write * direction rejects any REAL field with this name — the name is the marker. */ declare const ROOT_STORAGE_NAME = "__root"; type StoredFieldBase = { name: string; required?: boolean; nullable?: boolean; /** Human guidance — mirrors FieldBase.description. */ description?: string; /** Default VALUE (annotation-level) — mirrors FieldBase.default. */ default?: unknown; }; /** One element of `kind_definition.data` — FieldSchema + name, ref targets removed. */ type StoredFieldElement = (StoredFieldBase & { type: "string" | "boolean"; }) | (StoredFieldBase & { type: "number"; min?: number; max?: number; step?: number; }) | (StoredFieldBase & { type: "string[]"; values?: string[]; open?: boolean; }) | (StoredFieldBase & { type: "number[]" | "boolean[]"; }) | (StoredFieldBase & { type: "json"; }) | (StoredFieldBase & { type: "json[]"; }) | (StoredFieldBase & { type: "array"; }) | (StoredFieldBase & { type: "object"; }) | (StoredFieldBase & { type: "inline_object"; fields: StoredFieldElement[]; open?: boolean; }) | (StoredFieldBase & { type: "record"; values: RecordValueType; }) | (StoredFieldBase & { type: "enum"; values: string[]; open?: boolean; }) | (StoredFieldBase & { type: "union"; scalars: Array<"string" | "number" | "boolean">; /** Marker that this union's kind refs live in edges (positions ordered). */ hasKinds?: boolean; }); /** One `content_ir.kind_edge` row (child resolved to an id at insert time). */ type KindEdgeSpec = { /** Field PATH in the parent's `data` (dot-notation into inline_objects). */ fieldPath: string; /** Child kind slug — insert resolves this to `child_definition_id`. */ childKind: string; /** Union (anyOf) ordering for array refs; null for a single object ref. */ position: number | null; }; /** The full write payload for one kind: the ordered data array + its edges. */ type KindStorageShape = { data: StoredFieldElement[]; edges: KindEdgeSpec[]; }; declare class KindStorageError extends Error { constructor(message: string); } /** * MIGRATION / write direction. Order of `data` follows `Object.entries(fields)` * — the caller supplies the KindSchema whose field order is authoritative * (prefer the compiled `system-kinds.ts` order for system kinds during the * one-time flexible_data migration; jsonb order is the fallback for user kinds). */ declare function kindSchemaToStorage(schema: KindSchema): KindStorageShape; /** ADAPTER / read direction — the exact inverse of `kindSchemaToStorage`. */ declare function storageToKindSchema(kind: string, shape: KindStorageShape): KindSchema; /** * The dual gate — Arman's law as executable code: a kind is only `is_active` * when its canonical `sample_data` passes BOTH systems. * * 1. Structural (Pydantic): the sample validates against the kind's * `emitted_json_schema`. Python's Pydantic is the AUTHORITATIVE owner of * this leg, but it reads the SAME materialized `emitted_json_schema` — so * this TS ajv check and the Python check validate the same sample against * the same schema and agree by construction. A disagreement IS the * screamer (a schema Pydantic can't express, or an ajv/Pydantic gap). * 2. Render (UI): the sample lights up the kind's real component. TS is the * AUTHORITATIVE owner of this leg. It is a PROXY for a DOM render, not a * DOM render. Exactly what it checks, and nothing more: * · the kind has something to render with (`legacyBlockType` or * `component`); * · for BRIDGED kinds, `toLegacyServerData(sample)` returns a plain * object that is SEMANTICALLY non-empty — see * `describeUnrenderableBridgeOutput` for the exact predicate; * · BRIDGELESS kinds pass with a recorded caveat — nothing is verified. * * This catches the 2026-07-04 "No flashcards available yet" class: a * bridge that returns `undefined`, `{}`, `{language:"json"}` (the raw * code-region annotation), or an object whose every value is empty. * * What it does NOT do — stated plainly, because a gate that overclaims is * worse than no gate: it does not mount the component, and it cannot know * WHICH key carries the payload. `{title:"Cell Biology", cards:[]}` passes * this leg on `title` alone. Proving the payload key is populated needs * per-kind knowledge the gate deliberately does not have; a DOM-level * render check is the deeper leg, deferred to an RTL harness. * * Both legs necessary, neither sufficient. Fail either → `isActive: false` and * the caller reports it loudly (Error Inspector, `content-ir`) and holds the * row out of production. This module is PURE (deps injected) so it runs in the * harness, in CI, and in a browser author-save alike. * * Ownership split (the 2026-07-05 kind-registry storage design, since deleted; the live contract is the code below): the caller writes the outcome * to the LIVE `content_ir.kind_definition` row's `is_active`; the canonical * `_version_capture` trigger snapshots that state into `history.row_versions` * (never a post-hoc history mutation). */ /** The facets the render leg needs — a structural subset of KindDefinition. */ interface DualGateDefinition { legacyBlockType?: string; toLegacyServerData?: (envelope: CanonicalBlockIR) => Record | undefined; component?: { load: () => Promise; }; } /** * The render leg's second satisfier: an ACTIVE `role='output'` component * resolved from `content_ir.kind_component`. Feed this from * `resolveComponent(kind, "web", "output")`. * * Why this exists: the render leg originally consulted only the compiled TS * registry, so an agent-authored kind whose renderer is a `source='db'` row was * structurally unable to pass — `definition` came back null and the gate said * "no component" about a kind that had a live, working one. Six authored kinds * (wine_tasting, employee_card, employee_roster, employee_of_the_week, * flashcard_deck, arman_video_prompt) sat permanently inactive because of it. * * NOT a mirror of `content_ir.evaluate_kind_activation`. The SQL render leg is * presence-only — it checks that an active `role='output'` row exists, because * SQL cannot execute a TypeScript bridge. THIS leg is strictly stronger: for a * compiled kind it also runs `toLegacyServerData` and rejects semantically * empty output (the "No available" class). * * The asymmetry is deliberate and bounded: SQL is the FLOOR (necessary, and * sufficient for DB-authored components, which own no bridge), while this leg * is the CEILING for compiled kinds. Consequence to know: activating a COMPILED * kind whose bridge is broken would pass the RPC and fail here. Compiled kinds * are activated by developers through `scripts/shape/activate-kinds.ts`, which * runs this leg — the studio control only ever reaches owner-authored kinds. * If that ever stops being true, the browser control must run this gate before * enabling its button. */ interface DualGateResolvedComponent { componentKey: string; /** The row's own render-trust verdict (R6). Inactive rows do not satisfy. */ isActive: boolean; /** "bundled" | "db" — reported in the leg detail, never gates the verdict. */ source: string; } interface DualGateInput { kind: string; /** The canonical instance (kind_definition.sample_data). */ sample: Record; /** The materialized kind_definition.emitted_json_schema (plain, no __kind). */ emittedJsonSchema: unknown; /** The registry definition for the kind (or null when unregistered). */ definition: DualGateDefinition | null; /** * The resolver's `(kind, web, output)` answer, when the caller has one. * Omit (or pass null) to check the compiled registry alone. */ resolvedComponent?: DualGateResolvedComponent | null; /** * True when the kind is a generated data-only contract * (`metadata.family` ∈ workflow_io | tool_io | action_io | agent_io). * Those are passed between nodes and never rendered, so the render leg is * structurally inapplicable — the same `n/a` doctrine the shape doctor uses. * Failing them would be noise, and noise erodes the gate. */ dataOnly?: boolean; } interface LegResult { ok: boolean; detail?: string; } interface DualGateResult { /** True only when BOTH legs pass — the value the caller writes to is_active. */ isActive: boolean; structural: LegResult; render: LegResult; } /** * The structural leg, exported on its own so the shape doctor * (`shape-doctor.ts`) RECOMPUTES gate validation with the exact same ajv * config + marker semantics as activation (verbatim first, marker-free retry * second) — never a parallel validator. `sample` is `unknown` (not `Record`) because kind examples may * legitimately be scalars/arrays (workflow I/O kinds like `text`/`number`). */ declare function validateStructuralLeg(sample: unknown, emittedJsonSchema: unknown): LegResult; declare function runKindDualGate(input: DualGateInput): DualGateResult; /** * One-line, Error-Inspector-ready reason for a failed gate (empty string when * it passed). The caller feeds this to `captureError({ source: "content-ir" })` * and sets `is_active=false`. */ declare function describeDualGateFailure(kind: string, result: DualGateResult): string; /** * Kind validation behind a schema-source port (Matrx Alchemy ALC-13). * * `createKindValidator(source)` answers the one-call question "is this value a * valid X?" for a caller holding a kind SLUG and a payload, and nothing else — * an agent result, a surface write, an import. The HOST supplies the schema * through `SchemaSourcePort` (the browser reads `content_ir.kind_definition` * through Supabase; a server or a test hands in whatever it holds). This * package never imports a database client. * * ── A skip is never a pass ────────────────────────────────────────────────── * * `checked` is reported separately from `ok`, mirroring Python's `KindCheck` * (`matrx_graph/kinds.py`) and using the SAME degraded-reason vocabulary, so * the two runtimes describe the same failure the same way. A caller that reads * only `ok` gets `false` when the check could not run — the safe direction. * * Validation itself delegates to `validateStructuralLeg` — the activation * gate's own leg — so this is never a parallel validator. One ajv config, one * marker rule, one answer. * * Deliberately NOT reported here: `kind_definition.is_active`. That is a * RENDER-trust verdict, not a statement about the contract — an inactive * kind's schema is perfectly checkable. * * State: each validator owns its own short-lived schema cache (instance state, * never module state), so two hosts in one graph never share or poison a cache. */ /** JSON as the schema source hands it over. */ type Json = null | boolean | number | string | Json[] | { [key: string]: Json; }; /** * Why a schema could not be had, or could not be used. Identical to Python's * `KindDegradedReason` (`matrx_graph/kinds.py`) minus `kind_inactive`. */ type SchemaUnavailableReason = "kind_not_registered" | "schema_unavailable" | "schema_invalid" | "catalog_unreachable"; /** * The host's schema source. Implemented ONCE per host (the browser app reads * the kind catalog; a test hands in a fixture). * * Contract: * - `{ schema, version }` when the kind is registered and carries an emitted * JSON Schema; * - `{ unavailable: "kind_not_registered" }` when no live kind has that slug; * - `{ unavailable: "schema_unavailable" }` when the kind has no schema; * - `{ unavailable: "catalog_unreachable" }` when the catalog could not be * read. A source that THROWS is treated the same way by the validator. * - `schema_invalid` is normally decided by the validator (the schema does not * compile); a source may also report it when it knows the stored schema is * malformed. */ interface SchemaSourcePort { kindSchema(kindKey: string, signal: AbortSignal): Promise<{ schema: Json; version: string; } | { unavailable: SchemaUnavailableReason; }>; } interface KindValidationResult { kind: string; /** False when the check could not run at all. A skip is NEVER a pass. */ checked: boolean; /** True only when `checked` AND the value satisfied the schema. */ ok: boolean; /** ajv messages, or a single explanatory line when the check was skipped. */ errors: string[]; /** Non-null exactly when `checked === false`. */ degradedReason: SchemaUnavailableReason | null; } interface KindValidator { /** * Validate `value` against the live contract for `kind`. Never throws for an * unknown kind, a missing schema or an unreachable catalog — those are * degraded verdicts, because a thrown error at a validation boundary tends * to be caught and swallowed into a silent pass. A malformed argument (an * empty kind) still throws. */ validate(value: unknown, kind: string, signal?: AbortSignal): Promise; /** * The cached schema for `kind`, or `null` when it cannot be had. Never * throws. For callers that SHOW the contract (a tool spec summarizing a * target's schema) without paying a second read for a contract `validate` * is about to fetch anyway. Same cache, same TTL. */ cachedSchema(kind: string, signal?: AbortSignal): Promise; /** Drop one kind's cached schema, or all of them. Call after a local write. */ invalidate(kind?: string): void; } interface KindValidatorOptions { /** * How long a fetched schema is reused. Schema reads are a network hop and * validation runs in loops, but a definition edit bumps its version and * must be picked up without a reload. Default 30 s. */ cacheTtlMs?: number; /** Clock, injectable for tests. Default `Date.now`. */ now?: () => number; } /** Build a validator over one host's schema source. */ declare function createKindValidator(source: SchemaSourcePort, options?: KindValidatorOptions): KindValidator; /** Generated from matrx-graph's canonical Python KIND_DISPOSITIONS tuple. */ declare const KIND_DISPOSITIONS: readonly ["record", "envelope", "receipt", "proposal", "prose"]; type KindDisposition = (typeof KIND_DISPOSITIONS)[number]; export { KIND_DISPOSITIONS, KindStorageError, ROOT_STORAGE_NAME, createKindValidator, describeDualGateFailure, kindSchemaToStorage, runKindDualGate, storageToKindSchema, validateStructuralLeg }; export type { DualGateDefinition, DualGateInput, DualGateResolvedComponent, DualGateResult, Json, KindBlockProps, KindDefinition, KindDisposition, KindEdgeSpec, KindStorageShape, KindTier, KindValidationResult, KindValidator, KindValidatorOptions, LegResult, SchemaSourcePort, SchemaUnavailableReason, StoredFieldElement, TableKindFacet };