import * as React from 'react'; import { ReactNode } from 'react'; /** * Kind Directives — the grammar, the shell, and the position law (kernel edition). * * ONE system (Arman, 2026-08-23): the Matrx Envelope/Directive protocol and the * Content IR kind system are one system. A directive is an ordinary kind * instance — `{ "__kind": "directive_v1__", "items": [...] }` — * whose registered shape additionally carries execution semantics on the * server. This module is the PURE half, ported verbatim from the aidream source * of record (`packages/matrx-graph/matrx_graph/content_ir/directives.py`) so * every UI parses the grammar identically. It lives in the KERNEL because a * host that can parse a kind must be able to recognize a directive without a * second copy of these rules (matrx-frontend carried the only copy until * 2026-09-08; Workflow Studio had none and rendered directives as raw text). * * THE SLUG GRAMMAR — `directive_v__`: * - `directive_v` is a RESERVED prefix; a hand-authored kind may never claim it. * - `` comes from a CLOSED vocabulary, so parsing is unambiguous even * though nouns contain underscores: `directive_v1_reference_create_task` is * `(reference, "create_task")`. * - capability is DERIVED from the class, never stored twice. */ /** The CLOSED class vocabulary. Closed is what makes the grammar parseable. */ declare const CLASSES: readonly ["reference", "view", "create", "update", "delete", "action", "validation", "secret"]; type DirectiveClass = (typeof CLASSES)[number]; type DirectiveCapability = "pure" | "sensitive" | "side_effect"; /** A parsed directive slug. `slug` round-trips through `buildDirectiveSlug`. */ interface DirectiveSlug { slug: string; version: number; directiveClass: DirectiveClass; noun: string; capability: DirectiveCapability; /** Executes at an agent's output root (THE position law, half one). */ executes: boolean; /** Resolves to a live value inside content (THE position law, half two). */ inContent: boolean; } /** * Decode a Kind Directive — THE one entry point on every client. * * `decodeDirective()` recognizes the shell (current OR, for stored content * only, the retired 4-key one), reads the slug, and derives class + noun. A * caller only ever sees a parsed {@link DecodedDirective}; nothing downstream * inspects a raw shell again. * * ITEM VALIDATION IS THE SERVER'S. The registered item models live in aidream * (`services/content_ir_directives/registry.py`) and validate on apply; a * client-side copy of ~120 item models is exactly the drift the merge exists * to kill. The client parses identity, routes, and renders. Mirror of aidream * `services/content_ir_directives/decode.py`. */ interface DecodedDirective { /** The parsed slug — class, noun, capability and the position law. */ readonly parsed: DirectiveSlug; readonly slug: string; readonly directiveClass: DirectiveClass; readonly noun: string; readonly items: Record[]; /** The two-key shell, normalized — what a confirm POST round-trips. */ readonly shell: Record; /** True when this arrived in the retired 4-key shell and was translated. */ readonly legacyShell: boolean; } /** * THE AUTO-VIEW's naming half: every enrolled noun "instantly has a view" — * the prefix rule gives it a renderer, and this gives it a NAME. A shape the * client has never heard of must still read as "Create Agent · Agents", never * as the raw `agent` token and never as a slug. * * The catalog (`platform.entity_types` → the server's directive catalog) is the * authority for `label` / `family` / `title_column`; a host that mirrors it * passes a {@link DirectiveNounCatalog}. Without one — or for a noun the * catalog does not carry (a Kind Action like `plan_tree`) — the name degrades * to a title-cased token: legible, honestly derived, never blank. */ interface DirectiveNounEntry { /** The noun's human name — "Agent", "Plan tree". */ label?: string | null; /** The catalog family ("Agents"). */ family?: string | null; /** Which field of a row names it (`name`, `title`, …). */ titleColumn?: string | null; } /** A host-supplied lookup over the mirrored catalog. `undefined` = not carried. */ type DirectiveNounCatalog = (noun: string) => DirectiveNounEntry | undefined; /** * THE DIRECTIVE⇄KIND SEAM, client side. * * Arman, 2026-08-26: the envelope / Matrx-Actions system and the Shape (kind) * system are ONE system with several methods inside it. They meet at the ITEM. * A directive is a CONTAINER; its items are the payload, and when the server's * item model is already a `KindModel`, that payload IS a registered kind * instance — so the kind system already knows how to validate, render and copy * it. * * The seam is SERVER-DERIVED (`ShapeSpec.item_kind`). Since 2026-09-08 the ONE * kind endpoint publishes every directive shape as a KindDescriptor whose * `items` EDGE names the item kind, so a client reads it from the same * catalog it already holds — {@link directiveItemKindFromEdges}. A host that * mirrors the directive catalog manifest instead passes its map as a * {@link DirectiveItemKindLookup}. `null` is HONEST, never a gap-filler. */ /** A host-supplied lookup: slug → the kind ONE item is, or null. */ type DirectiveItemKindLookup = (slug: string) => string | null; interface DirectiveApplyResult { applied: number; failed: number; /** * The server's own receipt sentence ("Deleted task “Ship it”."), shown in * place of a bare count. Never composed client-side. */ message?: string | null; /** * The records the apply created or changed, so the tally is a way IN to them * (no dead ends). Rendered through `renderRecord`; absent → the sentence alone. */ records?: DirectiveRecordRef[]; /** * For an UPDATE: what each item's fields held just BEFORE the write, by item * index (`null` for an item the server did not record). The card draws the * applied change from this record — never from a live re-read, which after the * apply answers the NEW value (G6A review, 2026-10-02: "(no change)" for a * change that happened). */ before?: ReadonlyArray | null>; } /** * What the server's action ledger says about ONE block, read on mount so a * reloaded page never offers "Apply" for work that already ran (reviewer * defect 5, 2026-10-02). * * - `not_applied` — still to do (or partly done: the batch re-applies, and the * server replays the members it already holds). The card offers Apply. * - `in_flight` — another request is applying it right now. The card says it * is finishing and asks again shortly; it offers neither Apply nor a tally. * - `applied` — the ledger holds every item. The card shows the tally exactly * as a fresh apply would: the ledger's own sentence and the records, as doors. */ interface DirectiveApplyState { state: "not_applied" | "in_flight" | "applied"; /** The ledger row's sentence ("Created task."), never composed client-side. */ message?: string | null; /** * The records the LATEST apply wrote — the tally's doors. After "Run again" * that is the newest copy, never the first. */ records?: DirectiveRecordRef[]; /** * How many times the ledger holds this block as applied: 1 for the original, * one more for every "Run again". Absent → 1. A create that ran twice made * two records, and the card says so (reviewer, 2026-10-02). */ copies?: number; /** * For an applied UPDATE: each item's replaced values, by item index, from the * ledger row (`null` where the ledger has none — a row from before 2026-10-02). * Absent → the card says only what each field was set to. */ before?: ReadonlyArray | null>; } /** One record a directive names: the noun it is, and its id. */ interface DirectiveRecordRef { noun: string; id: string; } interface DirectiveRecordProps extends DirectiveRecordRef { /** * What to show until (or unless) the host resolves the live name — * "Task 1a2b3c4d". Never blank, never "Item". */ fallback: string; /** Where it is drawn: an item row, the confirm dialog, or the applied tally. */ context: "row" | "dialog" | "applied"; /** * True when the record is (about to be) in the trash — the host may still * name it but should not promise it opens as a live record. */ trashed?: boolean; } /** What the person is being asked to approve — everything the card knows. */ interface DirectiveAskRequest { directive: DecodedDirective; /** The items, exactly as they will be sent. */ items: Record[]; /** The noun's human label ("Task"), from the host's catalog when it has one. */ nounLabel: string; /** * True for "Run again": the ledger already holds this block, and a yes runs * it a SECOND time (`force`) — another copy for a create, the same fields * written again for an update, another delete for a restored record. The * question must say so. */ again?: boolean; } /** * The shell a card hands its host — the two keys, plus WHERE it sits. * * `conversationId` is the conversation the card renders in (a chat message), * absent in a person's own content (a note). It is the idempotency NAMESPACE: * a card in an agent's message must apply under the SAME key as that * proposal's Approve button, or the one action could run twice. */ interface DirectiveShell { __kind: string; items: Record[]; conversationId?: string; /** "Run again": skip the ledger's dedup and apply a second time. */ force?: boolean; } interface DirectiveOpenItemOptions { /** The item, `__kind`-stamped when the shape has an item kind. */ item: Record; /** The content-IR kind this item IS, or null when the shape has none. */ itemKind: string | null; /** What the item is called — the same title the card shows. */ title: string; /** "Run Create agent definition · item 1 of 1". */ subtitle: string | null; directive: DecodedDirective; index: number; total: number; } interface DirectiveCopyProps { /** * What the control is called to a screen reader. Never an id: for a record it * is type-generic ("this task"); a host with the record's live name replaces * it through `record`. */ label: string; /** The existing record this control copies, when it copies one — to name it. */ record?: DirectiveRecordRef; /** The value to copy — the whole shell or one item. */ value: unknown; /** The item's kind, when copying an item. */ kind?: string | null; size: "icon" | "xs"; } interface DirectiveHost { /** * Apply a side-effect directive as the user (the server re-validates every * item against the registered model and runs the ONE handler; idempotent). * Reject with an Error whose message is safe to show. */ confirm?: (shell: DirectiveShell) => Promise; /** * Ask the person BEFORE anything runs, naming the consequence (the records, * what changes). Resolve `true` to go ahead, `false` to leave the card as it * was. The Apply control stays idle while this is open — nothing reads as * "applying" until the person has said yes. Absent → `confirm` is called * directly (a host that asks inside `confirm` keeps working, but its card * will read "Applying…" while its dialog is up). */ ask?: (request: DirectiveAskRequest) => Promise; /** * Has this exact block already been applied? Read ONCE when the card mounts * (and again while the answer is `in_flight`), so the card survives a reload: * the server ledger, not component state, is what knows. The identity is the * server's (it hashes the validated items in the same namespace `confirm` * applies in), so a host asks — it never computes. Batch on the host side if * a page carries many cards. Reject with an Error when the ledger cannot be * read: the card reports it and offers Apply, which is safe because `confirm` * is idempotent. Absent → the card starts at Apply, as before. */ applyState?: (shell: DirectiveShell) => Promise; /** * Read the CURRENT values of ONE existing record, so an update says what it * replaces — "Status open → done", never just "→ done" (reviewer, * 2026-10-02). Resolve `null` when the record cannot be read (no access, no * such record, a noun the host cannot read); reject only for a real failure. * Absent → an update row shows the new values alone, as before. */ readRecord?: (ref: DirectiveRecordRef) => Promise | null>; /** * Draw ONE existing record by its live name, as a way to open it. Used on * update/delete rows (the record that will change), in the host's own ask * dialog if it likes, and on the applied tally (what was written). Absent → * the fallback text, unlinked. */ renderRecord?: (props: DirectiveRecordProps) => ReactNode; /** Open ONE item to read it properly before approving the write. */ openItem?: (options: DirectiveOpenItemOptions) => void; /** Host copy controls. */ renderCopy?: (props: DirectiveCopyProps) => ReactNode; /** * The app's word for one stored value of one pick-list field — the SAME word * the record's own screens and the write forms show ("incomplete" on a task * reads "Inbox"). One value, one display word everywhere (G8A review, * 2026-10-02: the card said "Status incomplete → completed"). Return null for * a field that is not a pick-list: its value is shown as written. Absent → * every value is shown as stored. */ valueLabel?: (ref: DirectiveValueRef) => string | null; /** The mirrored noun catalog (label / family / title column). */ nouns?: DirectiveNounCatalog; /** slug → the kind ONE item is (THE DIRECTIVE⇄KIND SEAM), or null. */ itemKind?: DirectiveItemKindLookup; /** Where a decode failure or an unhonourable shell is reported. Default: console.error. */ reportError?: (message: string) => void; /** * Plain words for a failed apply the host recognizes ("This button can't * create a task yet." + "Create it from Tasks instead."). The card never shows * the server's raw text as its sentence: it shows these words — or its own * verb-aware default — and keeps the raw text behind "Details". Return null * to use the default. */ explainFailure?: (failure: DirectiveFailure) => DirectiveFailureWords | null; } /** One stored value of one field of one record type. */ interface DirectiveValueRef { noun: string; field: string; value: string; } /** One failed apply, as the card knows it. */ interface DirectiveFailure { /** The server's / host's own text — technical; shown only behind Details. */ raw: string; directiveClass: string; noun: string; /** The noun's display label ("Task"). */ nounLabel: string; /** Items that failed, when some applied and some did not. */ failed?: number; /** Items in the block. */ total?: number; } /** What failed and what to do — each one short sentence. */ interface DirectiveFailureWords { what: string; next: string; /** * The text behind "Details", when the host words it (e.g. the server's field * names read as the form labels them — "Title is required", never * "name is required"). Absent → the raw text. */ details?: string; } /** Where a directive renders — today, the conversation it sits in (if any). */ interface DirectiveScope { conversationId?: string | null; } /** * Tell every card below WHERE it renders. A host draws a chat message's * directives inside one of these (or passes `conversationId` to * `DirectiveRender`), so the card applies in that conversation's namespace — * the same key the agent's proposal approves under. A note mounts none. */ declare function DirectiveScopeProvider({ conversationId, children, }: DirectiveScope & { children: ReactNode; }): React.JSX.Element; declare function useDirectiveScope(): DirectiveScope; interface DirectiveHostProviderProps { host: DirectiveHost; children: ReactNode; } declare function DirectiveHostProvider({ host, children }: DirectiveHostProviderProps): React.JSX.Element; /** * The wired directive host. Unlike the render host this does NOT throw when * unwired: a directive still renders (named, itemised, copy-less, apply-less) * — degrading the chrome is honest, hiding the content is not. */ declare function useDirectiveHost(): DirectiveHost; export { DirectiveHostProvider as j, DirectiveScopeProvider as o, useDirectiveScope as r, useDirectiveHost as u }; export type { DirectiveHost as D, DecodedDirective as a, DirectiveClass as b, DirectiveFailure as c, DirectiveFailureWords as d, DirectiveRecordRef as e, DirectiveApplyResult as f, DirectiveApplyState as g, DirectiveAskRequest as h, DirectiveCopyProps as i, DirectiveHostProviderProps as k, DirectiveOpenItemOptions as l, DirectiveRecordProps as m, DirectiveScope as n, DirectiveShell as p, DirectiveValueRef as q };