/** * AI cast — the framework PRIMITIVE that teaches a {@link DocumentGraph} how to * speak to a model, and validates what the model proposes coming back. * * ONE-LINE SPEC (the boundary this module honors): * **"LiteShip teaches graphs how to speak to models; products decide whether * model suggestions become action."** * * This is the PRIMITIVE half of "AI" — NOT the producer. The flow it owns: * * DocumentGraph * → castContext() : a deterministic, content-addressed {@link AIContext} * (the model-facing prompt + tool/output schema + a * token-budgeted graph summary) [cast OUT] * → (a model fills the advertised schema and proposes a GraphPatch / * GeneratedUITree — OUTSIDE this module; the framework does NOT call it) * → validateGraphPatchProposal() / validateGeneratedUIProposal() * : validate + preview, then MINT a {@link * ValidatedProposal} (the security envelope) [cast IN] * → applyValidatedPatch() : a SEPARATE, host-authorized step that ONLY * accepts a validation-minted proposal. * * THE LOAD-BEARING RULE: there is NO path from raw model output to graph * mutation that skips validation. `applyValidatedPatch` cannot be called with * anything but a {@link ValidatedProposal}, which only the validators in this * module can mint (the apply token's witness is private — see * `validated-output.ts`). The framework EXPOSES apply but NEVER invokes it * itself. * * PROPOSAL-SCHEMA CLOSURE: the output schema the {@link AIContext} advertises * for "propose a GraphPatch" is the SAME `GraphPatch` shape the framework * validates on the way back in. Cast-out schema and cast-in validator are two * faces of one type — see {@link graphPatchProposalSchema}. * * PURITY (== "no producer"): this module imports ZERO network / provider / * credential APIs. It is a pure, deterministic projection + validation kernel. * Same graph + same budget ⇒ same content-addressed context. * * @module */ import type { ContentAddress } from './brands.js'; import type { DocumentGraph } from './document-graph.js'; import { GraphPatch } from './graph-patch.js'; import type { ValidatedProposal, ProposalTarget } from './validated-output.js'; import type { ComponentCatalog, GeneratedUINode } from '@czap/_spine'; /** * A token-budgeted, deterministic summary of a {@link DocumentGraph}. Built by * walking the graph in topological order ({@link linearizeGraph}) and emitting * one terse line per node until the budget is spent — so the same graph + same * budget always yields the same summary (and the same content address). */ export interface GraphSummary { readonly _tag: 'GraphSummary'; /** The graph this summarizes (its content address). */ readonly base: ContentAddress; /** The token budget the summary was cut to. */ readonly tokenBudget: number; /** Estimated tokens the summary consumes (deterministic estimator). */ readonly estimatedTokens: number; /** Whether nodes were dropped to fit the budget. */ readonly truncated: boolean; /** Total node count in the graph (so the model knows what was elided). */ readonly nodeCount: number; /** One terse line per included node, in topological order. */ readonly lines: readonly string[]; } /** * The output-contract schema the {@link AIContext} advertises. Targets share one * shape: a JSON-Schema-ish descriptor plus the {@link ProposalTarget} tag that * routes a returned proposal to the matching validator. The GraphPatch schema is * the SAME `GraphPatch` the framework validates on the way back (closure). */ export interface ProposalSchema { readonly target: ProposalTarget; /** Human/model-readable name of the output contract. */ readonly name: string; /** JSON Schema describing the exact payload the model must return. */ readonly jsonSchema: Record; /** One-line description surfaced to the model. */ readonly description: string; } /** * The model-facing context cast OUT of a {@link DocumentGraph}. Deterministic and * content-addressed (`id` = fnv1a∘CanonicalCbor over the payload, the one repo * kernel) like every other cast. Carries: * - `summary`: the token-budgeted graph projection, * - `proposalSchemas`: the output contracts the model may fill (graph-patch * and/or generated-ui), advertised so the model knows EXACTLY what to return, * - `systemPrompt`: a deterministic prose framing of the above. * * It is INERT: nothing here calls a model. A producer feeds `systemPrompt` + * `proposalSchemas` to whatever model it routes to; the framework only built the * context. */ export interface AIContext { readonly _tag: 'AIContext'; readonly _version: 1; /** Content address of this context (over summary + schemas + prompt). */ readonly id: ContentAddress; /** The graph this context speaks for. */ readonly base: ContentAddress; readonly summary: GraphSummary; readonly proposalSchemas: readonly ProposalSchema[]; readonly systemPrompt: string; } /** Options for {@link castContext}. */ export interface CastContextOptions { /** Token budget for the embedded graph summary. Default 1024. */ readonly tokenBudget?: number; /** * Which output contracts to advertise to the model. Default: `['graph-patch']` * (the graph-native target). Add `'generated-ui'` when the host also exposes a * component catalog (pass it via {@link CastContextOptions.catalog}). */ readonly targets?: readonly ProposalTarget[]; /** * Host component catalog, REQUIRED when `'generated-ui'` is among the targets: * the advertised GeneratedUITree schema enumerates the catalog's components so * the model proposes only registered names. */ readonly catalog?: ComponentCatalog; } /** * Project a {@link DocumentGraph} to a token-budgeted {@link GraphSummary}. Walks * nodes in topological order (REUSING {@link linearizeGraph}; falls back to the * graph's own node order if the graph is cyclic — `linearizeGraph` returns the * partial sort plus the cycle, and a budgeted summary must still be emittable for * an in-progress/invalid graph). Emits one line per node until the next line * would exceed the budget. DETERMINISTIC: same graph + same budget ⇒ same * summary ⇒ same content address. */ export declare function summarizeGraph(graph: DocumentGraph, tokenBudget?: number): GraphSummary; /** * The output contract advertised for "propose a GraphPatch". This is the cast-OUT * face of the SAME `GraphPatch` the framework validates cast-IN — the model fills * exactly the shape {@link GraphPatch.validate} reads. Closure is structural: a * payload that satisfies this schema is a candidate `GraphPatch`; the validator * then re-runs the structural integrity check on its apply result. * * `base` is pinned to the context's graph so the model proposes a delta against * the graph it was shown. */ export declare function graphPatchProposalSchema(base: ContentAddress): ProposalSchema; /** * The output contract advertised for "propose a GeneratedUITree". Enumerates the * host catalog's registered component names so the model proposes only nodes the * host can render — the cast-OUT face of genui's `validateGeneratedUITree` * (cast-IN). This is the genui INSTANCE of the same propose→validate→envelope * discipline. */ export declare function generatedUIProposalSchema(catalog: ComponentCatalog): ProposalSchema; /** * Cast a {@link DocumentGraph} OUT to a deterministic, content-addressed * {@link AIContext}: a token-budgeted summary, the advertised output contracts * (GraphPatch always; GeneratedUITree when a catalog is supplied), and a prose * system prompt. NO model is called — this only BUILDS the context a producer * would feed to one. * * Determinism: same graph + same options ⇒ byte-identical context ⇒ same `id`. */ export declare function castContext(graph: DocumentGraph, options?: CastContextOptions): AIContext; /** A validation failure carrying the structured reason the proposal was rejected. */ export type ProposalRejection = { readonly ok: false; readonly target: ProposalTarget; readonly errors: readonly string[]; }; /** A passing validation — carries the minted {@link ValidatedProposal}. */ export type ProposalAcceptance = { readonly ok: true; readonly proposal: ValidatedProposal; }; /** The outcome of validating a model proposal: an acceptance (with envelope) or a rejection (with errors). */ export type ProposalResult = ProposalAcceptance | ProposalRejection; /** Re-exported from `document-graph-schema.ts` so existing `@czap/core` consumers keep the same import site. */ export { isWellFormedNode, DocumentGraphNodeSchema } from './document-graph-schema.js'; /** * Validate a model-proposed {@link GraphPatch} against the graph it was cast from, * then MINT a {@link ValidatedProposal} on success. This is the ONLY way to obtain * a graph-patch proposal a host can apply. * * It runs {@link GraphPatch.validate} (which previews the apply and re-checks * structural integrity — no cycles, no dangling edges) AND re-pins the patch's * `base` to the graph (a proposal must apply to the graph the model was shown). * Only when BOTH pass does it call `mintValidated` — so an unvalidated patch can * never become a `ValidatedProposal`. */ export declare function validateGraphPatchProposal(graph: DocumentGraph, patch: GraphPatch): ProposalResult; /** * The catalog-validation contract genui owns. * * RESOLVED (open question #2 — inject vs MOVE genui's `validateGeneratedUITree` * into core). INJECTION: the cast core does NOT depend on genui's runtime, and we * do NOT relocate genui's validator into core. The host (which already has * `@czap/genui`) passes its `validateGeneratedUITree` in as this function, so the * cast reuses genui's EXACT validation discipline with ZERO genui-file churn and * no core→genui (renderer) edge — preserving the product boundary and keeping the * core pure. genui's internals are untouched; this is the only seam between them. * * RESOLVED (open question #8 — the injected validator's error SHAPE). We pin the * narrowest contract that lets the cast surface a structured rejection: a success * or a failure carrying `error.message` (plus an optional `error.path`). This is * genui's existing `validateGeneratedUITree` return shape, so the host injects it * verbatim (no adapter). The cast NORMALIZES it into its own `ProposalRejection` * so both * targets reject through one `ProposalResult` shape — a foreign validator that * conforms to the type slots in cleanly, but a malformed model tree never reaches * a renderer because only `ok: true` mints the envelope. */ export type GeneratedUIValidator = (node: GeneratedUINode, catalog: ComponentCatalog) => { readonly ok: true; } | { readonly ok: false; readonly error: { readonly message: string; readonly path?: string; }; }; /** * Validate a model-proposed {@link GeneratedUINode} against a host catalog using * the host's genui validator, then MINT a {@link ValidatedProposal}. The genui * instance of the SAME envelope discipline — same gate, same minting, same * unforgeable token — so a UI tree cannot reach a host renderer un-validated any * more than a GraphPatch can reach a host mutator un-validated. * * The validator is injected (not imported) to keep the cast core free of the * genui renderer dependency; pass `validateGeneratedUITree` from `@czap/genui`. */ export declare function validateGeneratedUIProposal(node: GeneratedUINode, catalog: ComponentCatalog, validate: GeneratedUIValidator): ProposalResult; /** * Apply a VALIDATED graph-patch proposal to a graph. This is the host-authorized * mutation step the framework EXPOSES but NEVER calls itself. Its signature * DEMANDS a {@link ValidatedProposal} — which only {@link validateGraphPatchProposal} * can mint — so there is no path from raw model output to mutation that skips * validation. Before applying, it re-asserts the apply token binds to the exact * payload (defense-in-depth against post-validation tampering). * * Re-addresses through the one kernel ({@link GraphPatch.apply} → `sealGraph`), so * the result is indistinguishable from a graph authored fresh. * * APPLY-TIME GRAPH IDENTITY GUARD: a proposal is validated against a SPECIFIC graph * (its `payload.base` is pinned to that graph's id by {@link validateGraphPatchProposal}). * If the document graph advances between validate and apply, applying the validated * ops to a DIFFERENT graph could silently produce a structurally invalid result (an * edge valid in graph A may dangle in graph B). `GraphPatch.apply` itself ignores * `patch.base`, so we enforce the binding here: refuse to apply unless the apply-time * `graph.id` matches the `base` the proposal was validated against. The host remains * the authority over WHETHER to apply; this just stops a silent mis-apply against the * wrong graph (re-validate against the advanced graph to get a fresh proposal). */ export declare function applyValidatedPatch(graph: DocumentGraph, proposal: ValidatedProposal): DocumentGraph; /** * The AI cast namespace — the framework PRIMITIVE that casts a {@link DocumentGraph} * OUT to a model-facing {@link AIContext}, validates the patch / UI tree the model * proposes back IN (minting the {@link ValidatedProposal} security envelope), and * exposes (never invokes) the host-authorized apply step. * * "LiteShip teaches graphs how to speak to models; products decide whether model * suggestions become action." * * @example * ```ts * import { AICast, GraphPatch } from '@czap/core'; * * const ctx = AICast.castContext(graph, { tokenBudget: 512 }); // cast OUT * // ... a producer feeds ctx.systemPrompt + ctx.proposalSchemas to a model, * // which returns a candidate GraphPatch `patch` ... * const checked = AICast.validateGraphPatchProposal(graph, patch); // cast IN * if (checked.ok) { * // a SEPARATE host authority decides to admit it: * const next = AICast.applyValidatedPatch(graph, checked.proposal); * } * ``` */ export declare const AICast: { readonly castContext: typeof castContext; readonly summarizeGraph: typeof summarizeGraph; readonly graphPatchProposalSchema: typeof graphPatchProposalSchema; readonly generatedUIProposalSchema: typeof generatedUIProposalSchema; readonly validateGraphPatchProposal: typeof validateGraphPatchProposal; readonly validateGeneratedUIProposal: typeof validateGeneratedUIProposal; readonly applyValidatedPatch: typeof applyValidatedPatch; }; //# sourceMappingURL=ai-cast.d.ts.map