@ai-matrx/content-ir-react 0.17.1 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/host.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../host.ts","../host/error-report.ts","../host/host-types.ts","../host/ContentIrProvider.tsx","../directives/directive-host.tsx"],"sourcesContent":["/**\n * `@ai-matrx/content-ir-react/host` — the contract every consumer wires once:\n * the scream seam, the host/seam types, and the React provider.\n *\n * NOTE: this entry is stamped \"use client\" (it carries the provider). The\n * TYPES here are importable from anywhere; a server-side module that needs\n * only pure route logic imports `/route` instead.\n */\nexport * from \"./host/error-report\";\nexport * from \"./host/host-types\";\nexport * from \"./host/ContentIrProvider\";\n","/**\n * THE SCREAM SEAM.\n *\n * Every recovery path in this package is loud: a defective component row, a\n * failed resolver load, a kind whose component throws on a provisional value.\n * The package refuses to own where those screams land — matrx-frontend routes\n * them to the Error Inspector, the dashboard to its own log, the extension to\n * its background page. Each host binds one function.\n *\n * A host that binds a no-op has chosen silence, which is a defect in itself;\n * {@link consoleErrorReporter} is the honest default.\n */\n\nexport interface ContentIrErrorReport {\n /** Always `\"content-ir\"` — hosts key their capture stores on it. */\n source: \"content-ir\";\n message: string;\n name?: string;\n stack?: string;\n /** Free-form grouping key (the host's `relation`), e.g. `\"partial-kind\"`. */\n relation?: string;\n raw?: unknown;\n}\n\nexport type ContentIrErrorReporter = (report: ContentIrErrorReport) => void;\n\n/** The default when a host binds nothing: never silent. */\nexport const consoleErrorReporter: ContentIrErrorReporter = (report) => {\n console.error(`[content-ir] ${report.message}`, report.raw ?? \"\");\n};\n","/**\n * THE HOST CONTRACT — everything this package refuses to decide.\n *\n * The kernel (`@ai-matrx/content-ir`) proved the shape: extract the portable\n * part, let host capability enter through explicit named seams. The render\n * layer needs four things it must never own:\n *\n * 1. WHERE KIND DEFINITIONS COME FROM (a bundled table, `content_ir`, a REST\n * call). The route only asks \"is this kind registered, and does it carry a\n * legacy bridge / a partial-ready opt-in\".\n * 2. WHERE COMPONENT ROWS COME FROM — {@link ComponentResolutionSource}. The\n * package ships `ComponentResolver`, which any host can construct with its\n * own loaders; a host with its own resolver only has to satisfy the type.\n * 3. HOW A ROUTED BLOCK ACTUALLY DRAWS. `applyIrKindRoute` decides a block's\n * TYPE; turning that type into pixels is the host's dispatch table\n * (matrx-frontend's `BlockRenderer`, the dashboard's small map). The\n * package never imports a component library.\n * 4. WHERE SCREAMS LAND — {@link ContentIrErrorReporter}.\n *\n * No Next.js, no Redux, no Supabase, no router, no host error capture. If a\n * seam is missing, ADD A SEAM — never an import.\n */\n\nimport type { ReactNode } from \"react\";\nimport type { KindDefinition } from \"@ai-matrx/content-ir\";\nimport type { ContentIrErrorReporter } from \"./error-report\";\nimport type { ComponentResolutionSource } from \"../resolver/component-resolution.types\";\nimport type { DirectiveHost } from \"../directives/directive-host\";\n\n/**\n * The minimum a render block must expose for the kind route to work. Hosts\n * pass their own richer block type through; the route is generic over `T` and\n * preserves every extra field.\n */\nexport interface IrRoutableBlock {\n type: string;\n serverData?: Record<string, unknown>;\n metadata?: Record<string, unknown>;\n}\n\n/** A routable block that also carries its source text (the raw region). */\nexport interface IrRenderBlock extends IrRoutableBlock {\n content: string;\n language?: string;\n}\n\n/**\n * Where registered kinds come from. `getDefinition` is the only method the\n * pure route needs; the rest drive the granular repaint and the warm-up tick\n * that keeps a cold registry from being mistaken for \"this kind has no\n * component\".\n */\nexport interface KindDefinitionSource {\n getDefinition(kind: string): KindDefinition | undefined;\n /** Monotonic per-kind version — the repaint snapshot key. */\n getKindVersion(kind: string): number;\n subscribeKind(kind: string, listener: () => void): () => void;\n /** One definition load per app session. */\n ensureWarm(): Promise<void>;\n}\n\n/**\n * The rendering seams. Everything here returns host UI; the package supplies\n * the DECISIONS and the chrome-free structure around them.\n */\nexport interface ContentIrRenderSeams {\n /**\n * Draw a block the kind route has already typed. This is the host's dispatch\n * table — the single place a component key becomes a component.\n */\n renderBlock(block: IrRenderBlock): ReactNode;\n /**\n * THE FLOOR: render ANY JSON value as a human document. Reached when a kind\n * has no render-trusted component, when the value is not an object at all,\n * and by the generic structured view.\n *\n * This is deliberately a seam and not a bundled component. Rendering a value\n * WELL means prose through the host's markdown renderer, media through the\n * host's file handler, and uniform arrays through the host's data table —\n * all of which are host property. A host that has none of those can pass a\n * `<pre>`; it will be honest, just plain.\n */\n renderValue(props: StructuredValueRenderProps): ReactNode;\n /**\n * The \"still arriving\" indicator used by the provisional frame. Optional —\n * hosts without a shimmer get plain text.\n */\n renderShimmer?(text: string): ReactNode;\n /**\n * The honest \"this shape has no custom component yet\" notice. Optional: the\n * package draws a plain amber line when a host supplies nothing, and a host\n * with an icon set or its own callout component supplies that instead. It is\n * a seam and not a bundled component because this package depends on no icon\n * library.\n */\n renderNotice?(text: string): ReactNode;\n}\n\nexport interface StructuredValueRenderProps {\n value: unknown;\n /** The kind slug this value claims, when known. Honesty line only — never a renderer choice. */\n kind?: string;\n /** Why this shape has no custom view, in human words. */\n note?: string;\n /** Show the \"what this is / raw data\" footer. Default true. */\n footer?: boolean;\n}\n\n/**\n * One host, wired once, read by every component in this package through\n * {@link ContentIrRenderProvider}.\n */\nexport interface ContentIrHost extends ContentIrRenderSeams {\n kinds: KindDefinitionSource;\n components: ComponentResolutionSource;\n reportError: ContentIrErrorReporter;\n /**\n * The `kind_component.platform` this host resolves as. \"web\" for every\n * browser UI; the column already models \"react-native\" and friends, and a\n * host that lies here renders the wrong component everywhere.\n */\n platform: string;\n /**\n * The Kind Directives seams (apply / open item / copy / noun catalog / item\n * kind). Optional: a host without them still renders every directive card,\n * apply-less and copy-less — honest chrome, never hidden content. Provided\n * to the directive tier by `ContentIrRenderProvider`; a host that has not\n * adopted the render provider mounts `DirectiveHostProvider` directly.\n */\n directives?: DirectiveHost;\n}\n\n/**\n * The pure route's dependencies — a strict subset of {@link ContentIrHost},\n * because `applyIrKindRoute` is called OUTSIDE React (reducers, stream\n * accumulators, tests) where there is no provider to read.\n */\nexport interface KindRouteEnv {\n kinds: Pick<KindDefinitionSource, \"getDefinition\">;\n /**\n * `hasSettled` is optional and read ONLY to suppress a negative verdict from\n * a registry that has not loaded yet — see THE COLD-VERDICT RULE on\n * `ComponentResolutionSource`.\n */\n components: Pick<ComponentResolutionSource, \"resolve\"> &\n Partial<Pick<ComponentResolutionSource, \"hasSettled\">>;\n reportError: ContentIrErrorReporter;\n platform: string;\n}\n\n/** Narrow a host (or anything host-shaped) to what the pure route needs. */\nexport function routeEnvOf(host: ContentIrHost): KindRouteEnv {\n return {\n kinds: host.kinds,\n components: host.components,\n reportError: host.reportError,\n platform: host.platform,\n };\n}\n","\"use client\";\n\n/**\n * The one place a host is wired in. Everything React in this package reads its\n * capability from here — no module singleton, no import into an app.\n *\n * A host mounts this ONCE, high enough that every kind-rendering surface is\n * inside it:\n *\n * ```tsx\n * <ContentIrRenderProvider host={matrxContentIrHost}>\n * <App />\n * </ContentIrRenderProvider>\n * ```\n *\n * Reading it outside a provider THROWS rather than falling back to a default.\n * A silent default would render the wrong component for every kind in the app\n * and look like a data problem for weeks.\n */\n\nimport { createContext, useContext, useMemo, type ReactNode } from \"react\";\nimport type { ContentIrHost, KindRouteEnv } from \"./host-types\";\nimport { routeEnvOf } from \"./host-types\";\nimport { DirectiveHostProvider } from \"../directives/directive-host\";\n\nconst ContentIrHostContext = createContext<ContentIrHost | null>(null);\n\nexport interface ContentIrRenderProviderProps {\n host: ContentIrHost;\n children: ReactNode;\n}\n\nexport function ContentIrRenderProvider({\n host,\n children,\n}: ContentIrRenderProviderProps) {\n return (\n <ContentIrHostContext.Provider value={host}>\n <DirectiveHostProvider host={host.directives ?? {}}>{children}</DirectiveHostProvider>\n </ContentIrHostContext.Provider>\n );\n}\n\n/**\n * The wired host, or null. For the rare consumer that can legitimately run\n * without a provider because it was handed its sources explicitly (see\n * `useContentIrKindVersion`). Everything else uses {@link useContentIrHost}.\n */\nexport function useContentIrHostOrNull(): ContentIrHost | null {\n return useContext(ContentIrHostContext);\n}\n\n/** The wired host. Throws outside a provider — see the module doc. */\nexport function useContentIrHost(): ContentIrHost {\n const host = useContentIrHostOrNull();\n if (!host) {\n throw new Error(\n \"[content-ir-react] No ContentIrRenderProvider above this component. \" +\n \"Mount one at the app root with your host adapter (kind definitions, \" +\n \"component resolver, renderBlock, renderValue, reportError).\",\n );\n }\n return host;\n}\n\n/** The wired host narrowed to what the pure route functions take. */\nexport function useKindRouteEnv(): KindRouteEnv {\n const host = useContentIrHost();\n return useMemo(() => routeEnvOf(host), [host]);\n}\n","\"use client\";\n\n/**\n * THE DIRECTIVE HOST CONTRACT — what the directive tier refuses to decide.\n *\n * The package owns the DECISIONS (decode → renderer → card → floor, the\n * position law, never-null rendering) and the card itself. Four things stay\n * host property and enter through named seams:\n *\n * 1. APPLYING a side-effect directive — `POST /directives/confirm` as the\n * user, with the host's auth and base URL. Absent → the Apply control is\n * ABSENT (honest), never a dead button.\n * 2. OPENING an item to read it before approving — a window panel, a dialog,\n * a route. Absent → items are not clickable.\n * 3. COPY controls — host chrome (matrx-frontend's CopyButtons). Absent → none.\n * 4. The NOUN catalog + item-kind lookup — server-derived tables the host\n * mirrors (or reads from its kind catalog's `items` edges).\n * 5. ASKING (`ask`), NAMING a record (`renderRecord`), and READING the ledger\n * (`applyState`) — the consequence before the click, the doors, and the\n * state that survives a reload.\n *\n * A host that has adopted `ContentIrRenderProvider` passes `directives` on its\n * ContentIrHost; a host that has NOT (Workflow Studio, 2026-09-08) mounts\n * `DirectiveHostProvider` alone — the directive tier never requires the full\n * render host, because a chat bubble must be able to draw a directive card\n * identically everywhere, today.\n */\n\nimport { createContext, useContext, type ReactNode } from \"react\";\nimport type {\n DecodedDirective,\n DirectiveItemKindLookup,\n DirectiveNounCatalog,\n} from \"@ai-matrx/content-ir\";\n\nexport interface DirectiveApplyResult {\n applied: number;\n failed: number;\n /**\n * The server's own receipt sentence (\"Deleted task “Ship it”.\"), shown in\n * place of a bare count. Never composed client-side.\n */\n message?: string | null;\n /**\n * The records the apply created or changed, so the tally is a way IN to them\n * (no dead ends). Rendered through `renderRecord`; absent → the sentence alone.\n */\n records?: DirectiveRecordRef[];\n}\n\n/**\n * What the server's action ledger says about ONE block, read on mount so a\n * reloaded page never offers \"Apply\" for work that already ran (reviewer\n * defect 5, 2026-10-02).\n *\n * - `not_applied` — still to do (or partly done: the batch re-applies, and the\n * server replays the members it already holds). The card offers Apply.\n * - `in_flight` — another request is applying it right now. The card says it\n * is finishing and asks again shortly; it offers neither Apply nor a tally.\n * - `applied` — the ledger holds every item. The card shows the tally exactly\n * as a fresh apply would: the ledger's own sentence and the records, as doors.\n */\nexport interface DirectiveApplyState {\n state: \"not_applied\" | \"in_flight\" | \"applied\";\n /** The ledger row's sentence (\"Created task.\"), never composed client-side. */\n message?: string | null;\n /**\n * The records the LATEST apply wrote — the tally's doors. After \"Run again\"\n * that is the newest copy, never the first.\n */\n records?: DirectiveRecordRef[];\n /**\n * How many times the ledger holds this block as applied: 1 for the original,\n * one more for every \"Run again\". Absent → 1. A create that ran twice made\n * two records, and the card says so (reviewer, 2026-10-02).\n */\n copies?: number;\n}\n\n/** One record a directive names: the noun it is, and its id. */\nexport interface DirectiveRecordRef {\n noun: string;\n id: string;\n}\n\nexport interface DirectiveRecordProps extends DirectiveRecordRef {\n /**\n * What to show until (or unless) the host resolves the live name —\n * \"Task 1a2b3c4d\". Never blank, never \"Item\".\n */\n fallback: string;\n /** Where it is drawn: an item row, the confirm dialog, or the applied tally. */\n context: \"row\" | \"dialog\" | \"applied\";\n /**\n * True when the record is (about to be) in the trash — the host may still\n * name it but should not promise it opens as a live record.\n */\n trashed?: boolean;\n}\n\n/** What the person is being asked to approve — everything the card knows. */\nexport interface DirectiveAskRequest {\n directive: DecodedDirective;\n /** The items, exactly as they will be sent. */\n items: Record<string, unknown>[];\n /** The noun's human label (\"Task\"), from the host's catalog when it has one. */\n nounLabel: string;\n /**\n * True for \"Run again\": the ledger already holds this block, and a yes runs\n * it a SECOND time (`force`) — another copy for a create, the same fields\n * written again for an update, another delete for a restored record. The\n * question must say so.\n */\n again?: boolean;\n}\n\n/**\n * The shell a card hands its host — the two keys, plus WHERE it sits.\n *\n * `conversationId` is the conversation the card renders in (a chat message),\n * absent in a person's own content (a note). It is the idempotency NAMESPACE:\n * a card in an agent's message must apply under the SAME key as that\n * proposal's Approve button, or the one action could run twice.\n */\nexport interface DirectiveShell {\n __kind: string;\n items: Record<string, unknown>[];\n conversationId?: string;\n /** \"Run again\": skip the ledger's dedup and apply a second time. */\n force?: boolean;\n}\n\nexport interface DirectiveOpenItemOptions {\n /** The item, `__kind`-stamped when the shape has an item kind. */\n item: Record<string, unknown>;\n /** The content-IR kind this item IS, or null when the shape has none. */\n itemKind: string | null;\n /** What the item is called — the same title the card shows. */\n title: string;\n /** \"Run Create agent definition · item 1 of 1\". */\n subtitle: string | null;\n directive: DecodedDirective;\n index: number;\n total: number;\n}\n\nexport interface DirectiveCopyProps {\n label: string;\n /** The value to copy — the whole shell or one item. */\n value: unknown;\n /** The item's kind, when copying an item. */\n kind?: string | null;\n size: \"icon\" | \"xs\";\n}\n\nexport interface DirectiveHost {\n /**\n * Apply a side-effect directive as the user (the server re-validates every\n * item against the registered model and runs the ONE handler; idempotent).\n * Reject with an Error whose message is safe to show.\n */\n confirm?: (shell: DirectiveShell) => Promise<DirectiveApplyResult>;\n /**\n * Ask the person BEFORE anything runs, naming the consequence (the records,\n * what changes). Resolve `true` to go ahead, `false` to leave the card as it\n * was. The Apply control stays idle while this is open — nothing reads as\n * \"applying\" until the person has said yes. Absent → `confirm` is called\n * directly (a host that asks inside `confirm` keeps working, but its card\n * will read \"Applying…\" while its dialog is up).\n */\n ask?: (request: DirectiveAskRequest) => Promise<boolean>;\n /**\n * Has this exact block already been applied? Read ONCE when the card mounts\n * (and again while the answer is `in_flight`), so the card survives a reload:\n * the server ledger, not component state, is what knows. The identity is the\n * server's (it hashes the validated items in the same namespace `confirm`\n * applies in), so a host asks — it never computes. Batch on the host side if\n * a page carries many cards. Reject with an Error when the ledger cannot be\n * read: the card reports it and offers Apply, which is safe because `confirm`\n * is idempotent. Absent → the card starts at Apply, as before.\n */\n applyState?: (shell: DirectiveShell) => Promise<DirectiveApplyState>;\n /**\n * Read the CURRENT values of ONE existing record, so an update says what it\n * replaces — \"Status open → done\", never just \"→ done\" (reviewer,\n * 2026-10-02). Resolve `null` when the record cannot be read (no access, no\n * such record, a noun the host cannot read); reject only for a real failure.\n * Absent → an update row shows the new values alone, as before.\n */\n readRecord?: (ref: DirectiveRecordRef) => Promise<Record<string, unknown> | null>;\n /**\n * Draw ONE existing record by its live name, as a way to open it. Used on\n * update/delete rows (the record that will change), in the host's own ask\n * dialog if it likes, and on the applied tally (what was written). Absent →\n * the fallback text, unlinked.\n */\n renderRecord?: (props: DirectiveRecordProps) => ReactNode;\n /** Open ONE item to read it properly before approving the write. */\n openItem?: (options: DirectiveOpenItemOptions) => void;\n /** Host copy controls. */\n renderCopy?: (props: DirectiveCopyProps) => ReactNode;\n /** The mirrored noun catalog (label / family / title column). */\n nouns?: DirectiveNounCatalog;\n /** slug → the kind ONE item is (THE DIRECTIVE⇄KIND SEAM), or null. */\n itemKind?: DirectiveItemKindLookup;\n /** Where a decode failure or an unhonourable shell is reported. Default: console.error. */\n reportError?: (message: string) => void;\n /**\n * Plain words for a failed apply the host recognises (\"This button can't\n * create a task yet.\" + \"Create it from Tasks instead.\"). The card never shows\n * the server's raw text as its sentence: it shows these words — or its own\n * verb-aware default — and keeps the raw text behind \"Details\". Return null\n * to use the default.\n */\n explainFailure?: (failure: DirectiveFailure) => DirectiveFailureWords | null;\n}\n\n/** One failed apply, as the card knows it. */\nexport interface DirectiveFailure {\n /** The server's / host's own text — technical; shown only behind Details. */\n raw: string;\n directiveClass: string;\n noun: string;\n /** The noun's display label (\"Task\"). */\n nounLabel: string;\n /** Items that failed, when some applied and some did not. */\n failed?: number;\n /** Items in the block. */\n total?: number;\n}\n\n/** What failed and what to do — each one short sentence. */\nexport interface DirectiveFailureWords {\n what: string;\n next: string;\n}\n\nconst DirectiveHostContext = createContext<DirectiveHost | null>(null);\n\n/** Where a directive renders — today, the conversation it sits in (if any). */\nexport interface DirectiveScope {\n conversationId?: string | null;\n}\n\nconst DirectiveScopeContext = createContext<DirectiveScope>({});\n\n/**\n * Tell every card below WHERE it renders. A host draws a chat message's\n * directives inside one of these (or passes `conversationId` to\n * `DirectiveRender`), so the card applies in that conversation's namespace —\n * the same key the agent's proposal approves under. A note mounts none.\n */\nexport function DirectiveScopeProvider({\n conversationId,\n children,\n}: DirectiveScope & { children: ReactNode }) {\n return (\n <DirectiveScopeContext.Provider value={{ conversationId: conversationId ?? null }}>\n {children}\n </DirectiveScopeContext.Provider>\n );\n}\n\nexport function useDirectiveScope(): DirectiveScope {\n return useContext(DirectiveScopeContext);\n}\n\nexport interface DirectiveHostProviderProps {\n host: DirectiveHost;\n children: ReactNode;\n}\n\nexport function DirectiveHostProvider({ host, children }: DirectiveHostProviderProps) {\n return <DirectiveHostContext.Provider value={host}>{children}</DirectiveHostContext.Provider>;\n}\n\nconst EMPTY_HOST: DirectiveHost = {};\n\n/**\n * The wired directive host. Unlike the render host this does NOT throw when\n * unwired: a directive still renders (named, itemised, copy-less, apply-less)\n * — degrading the chrome is honest, hiding the content is not.\n */\nexport function useDirectiveHost(): DirectiveHost {\n return useContext(DirectiveHostContext) ?? EMPTY_HOST;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC2BO,IAAM,uBAA+C,CAAC,WAAW;AACtE,UAAQ,MAAM,gBAAgB,OAAO,OAAO,IAAI,OAAO,OAAO,EAAE;AAClE;;;AC0HO,SAAS,WAAW,MAAmC;AAC5D,SAAO;AAAA,IACL,OAAO,KAAK;AAAA,IACZ,YAAY,KAAK;AAAA,IACjB,aAAa,KAAK;AAAA,IAClB,UAAU,KAAK;AAAA,EACjB;AACF;;;AC1IA,IAAAA,gBAAmE;;;ACQnE,mBAA0D;AAqOtD;AApBJ,IAAM,2BAAuB,4BAAoC,IAAI;AAOrE,IAAM,4BAAwB,4BAA8B,CAAC,CAAC;AA4BvD,SAAS,sBAAsB,EAAE,MAAM,SAAS,GAA+B;AACpF,SAAO,4CAAC,qBAAqB,UAArB,EAA8B,OAAO,MAAO,UAAS;AAC/D;;;AD5OM,IAAAC,sBAAA;AAbN,IAAM,2BAAuB,6BAAoC,IAAI;AAO9D,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,GAAiC;AAC/B,SACE,6CAAC,qBAAqB,UAArB,EAA8B,OAAO,MACpC,uDAAC,yBAAsB,MAAM,KAAK,cAAc,CAAC,GAAI,UAAS,GAChE;AAEJ;AAOO,SAAS,yBAA+C;AAC7D,aAAO,0BAAW,oBAAoB;AACxC;AAGO,SAAS,mBAAkC;AAChD,QAAM,OAAO,uBAAuB;AACpC,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,kBAAgC;AAC9C,QAAM,OAAO,iBAAiB;AAC9B,aAAO,uBAAQ,MAAM,WAAW,IAAI,GAAG,CAAC,IAAI,CAAC;AAC/C;","names":["import_react","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../host.ts","../host/error-report.ts","../host/host-types.ts","../host/ContentIrProvider.tsx","../directives/directive-host.tsx"],"sourcesContent":["/**\n * `@ai-matrx/content-ir-react/host` — the contract every consumer wires once:\n * the scream seam, the host/seam types, and the React provider.\n *\n * NOTE: this entry is stamped \"use client\" (it carries the provider). The\n * TYPES here are importable from anywhere; a server-side module that needs\n * only pure route logic imports `/route` instead.\n */\nexport * from \"./host/error-report\";\nexport * from \"./host/host-types\";\nexport * from \"./host/ContentIrProvider\";\n","/**\n * THE SCREAM SEAM.\n *\n * Every recovery path in this package is loud: a defective component row, a\n * failed resolver load, a kind whose component throws on a provisional value.\n * The package refuses to own where those screams land — matrx-frontend routes\n * them to the Error Inspector, the dashboard to its own log, the extension to\n * its background page. Each host binds one function.\n *\n * A host that binds a no-op has chosen silence, which is a defect in itself;\n * {@link consoleErrorReporter} is the honest default.\n */\n\nexport interface ContentIrErrorReport {\n /** Always `\"content-ir\"` — hosts key their capture stores on it. */\n source: \"content-ir\";\n message: string;\n name?: string;\n stack?: string;\n /** Free-form grouping key (the host's `relation`), e.g. `\"partial-kind\"`. */\n relation?: string;\n raw?: unknown;\n}\n\nexport type ContentIrErrorReporter = (report: ContentIrErrorReport) => void;\n\n/** The default when a host binds nothing: never silent. */\nexport const consoleErrorReporter: ContentIrErrorReporter = (report) => {\n console.error(`[content-ir] ${report.message}`, report.raw ?? \"\");\n};\n","/**\n * THE HOST CONTRACT — everything this package refuses to decide.\n *\n * The kernel (`@ai-matrx/content-ir`) proved the shape: extract the portable\n * part, let host capability enter through explicit named seams. The render\n * layer needs four things it must never own:\n *\n * 1. WHERE KIND DEFINITIONS COME FROM (a bundled table, `content_ir`, a REST\n * call). The route only asks \"is this kind registered, and does it carry a\n * legacy bridge / a partial-ready opt-in\".\n * 2. WHERE COMPONENT ROWS COME FROM — {@link ComponentResolutionSource}. The\n * package ships `ComponentResolver`, which any host can construct with its\n * own loaders; a host with its own resolver only has to satisfy the type.\n * 3. HOW A ROUTED BLOCK ACTUALLY DRAWS. `applyIrKindRoute` decides a block's\n * TYPE; turning that type into pixels is the host's dispatch table\n * (matrx-frontend's `BlockRenderer`, the dashboard's small map). The\n * package never imports a component library.\n * 4. WHERE SCREAMS LAND — {@link ContentIrErrorReporter}.\n *\n * No Next.js, no Redux, no Supabase, no router, no host error capture. If a\n * seam is missing, ADD A SEAM — never an import.\n */\n\nimport type { ReactNode } from \"react\";\nimport type { KindDefinition } from \"@ai-matrx/content-ir\";\nimport type { ContentIrErrorReporter } from \"./error-report\";\nimport type { ComponentResolutionSource } from \"../resolver/component-resolution.types\";\nimport type { DirectiveHost } from \"../directives/directive-host\";\n\n/**\n * The minimum a render block must expose for the kind route to work. Hosts\n * pass their own richer block type through; the route is generic over `T` and\n * preserves every extra field.\n */\nexport interface IrRoutableBlock {\n type: string;\n serverData?: Record<string, unknown>;\n metadata?: Record<string, unknown>;\n}\n\n/** A routable block that also carries its source text (the raw region). */\nexport interface IrRenderBlock extends IrRoutableBlock {\n content: string;\n language?: string;\n}\n\n/**\n * Where registered kinds come from. `getDefinition` is the only method the\n * pure route needs; the rest drive the granular repaint and the warm-up tick\n * that keeps a cold registry from being mistaken for \"this kind has no\n * component\".\n */\nexport interface KindDefinitionSource {\n getDefinition(kind: string): KindDefinition | undefined;\n /** Monotonic per-kind version — the repaint snapshot key. */\n getKindVersion(kind: string): number;\n subscribeKind(kind: string, listener: () => void): () => void;\n /** One definition load per app session. */\n ensureWarm(): Promise<void>;\n}\n\n/**\n * The rendering seams. Everything here returns host UI; the package supplies\n * the DECISIONS and the chrome-free structure around them.\n */\nexport interface ContentIrRenderSeams {\n /**\n * Draw a block the kind route has already typed. This is the host's dispatch\n * table — the single place a component key becomes a component.\n */\n renderBlock(block: IrRenderBlock): ReactNode;\n /**\n * THE FLOOR: render ANY JSON value as a human document. Reached when a kind\n * has no render-trusted component, when the value is not an object at all,\n * and by the generic structured view.\n *\n * This is deliberately a seam and not a bundled component. Rendering a value\n * WELL means prose through the host's markdown renderer, media through the\n * host's file handler, and uniform arrays through the host's data table —\n * all of which are host property. A host that has none of those can pass a\n * `<pre>`; it will be honest, just plain.\n */\n renderValue(props: StructuredValueRenderProps): ReactNode;\n /**\n * The \"still arriving\" indicator used by the provisional frame. Optional —\n * hosts without a shimmer get plain text.\n */\n renderShimmer?(text: string): ReactNode;\n /**\n * The honest \"this shape has no custom component yet\" notice. Optional: the\n * package draws a plain amber line when a host supplies nothing, and a host\n * with an icon set or its own callout component supplies that instead. It is\n * a seam and not a bundled component because this package depends on no icon\n * library.\n */\n renderNotice?(text: string): ReactNode;\n}\n\nexport interface StructuredValueRenderProps {\n value: unknown;\n /** The kind slug this value claims, when known. Honesty line only — never a renderer choice. */\n kind?: string;\n /** Why this shape has no custom view, in human words. */\n note?: string;\n /** Show the \"what this is / raw data\" footer. Default true. */\n footer?: boolean;\n}\n\n/**\n * One host, wired once, read by every component in this package through\n * {@link ContentIrRenderProvider}.\n */\nexport interface ContentIrHost extends ContentIrRenderSeams {\n kinds: KindDefinitionSource;\n components: ComponentResolutionSource;\n reportError: ContentIrErrorReporter;\n /**\n * The `kind_component.platform` this host resolves as. \"web\" for every\n * browser UI; the column already models \"react-native\" and friends, and a\n * host that lies here renders the wrong component everywhere.\n */\n platform: string;\n /**\n * The Kind Directives seams (apply / open item / copy / noun catalog / item\n * kind). Optional: a host without them still renders every directive card,\n * apply-less and copy-less — honest chrome, never hidden content. Provided\n * to the directive tier by `ContentIrRenderProvider`; a host that has not\n * adopted the render provider mounts `DirectiveHostProvider` directly.\n */\n directives?: DirectiveHost;\n}\n\n/**\n * The pure route's dependencies — a strict subset of {@link ContentIrHost},\n * because `applyIrKindRoute` is called OUTSIDE React (reducers, stream\n * accumulators, tests) where there is no provider to read.\n */\nexport interface KindRouteEnv {\n kinds: Pick<KindDefinitionSource, \"getDefinition\">;\n /**\n * `hasSettled` is optional and read ONLY to suppress a negative verdict from\n * a registry that has not loaded yet — see THE COLD-VERDICT RULE on\n * `ComponentResolutionSource`.\n */\n components: Pick<ComponentResolutionSource, \"resolve\"> &\n Partial<Pick<ComponentResolutionSource, \"hasSettled\">>;\n reportError: ContentIrErrorReporter;\n platform: string;\n}\n\n/** Narrow a host (or anything host-shaped) to what the pure route needs. */\nexport function routeEnvOf(host: ContentIrHost): KindRouteEnv {\n return {\n kinds: host.kinds,\n components: host.components,\n reportError: host.reportError,\n platform: host.platform,\n };\n}\n","\"use client\";\n\n/**\n * The one place a host is wired in. Everything React in this package reads its\n * capability from here — no module singleton, no import into an app.\n *\n * A host mounts this ONCE, high enough that every kind-rendering surface is\n * inside it:\n *\n * ```tsx\n * <ContentIrRenderProvider host={matrxContentIrHost}>\n * <App />\n * </ContentIrRenderProvider>\n * ```\n *\n * Reading it outside a provider THROWS rather than falling back to a default.\n * A silent default would render the wrong component for every kind in the app\n * and look like a data problem for weeks.\n */\n\nimport { createContext, useContext, useMemo, type ReactNode } from \"react\";\nimport type { ContentIrHost, KindRouteEnv } from \"./host-types\";\nimport { routeEnvOf } from \"./host-types\";\nimport { DirectiveHostProvider } from \"../directives/directive-host\";\n\nconst ContentIrHostContext = createContext<ContentIrHost | null>(null);\n\nexport interface ContentIrRenderProviderProps {\n host: ContentIrHost;\n children: ReactNode;\n}\n\nexport function ContentIrRenderProvider({\n host,\n children,\n}: ContentIrRenderProviderProps) {\n return (\n <ContentIrHostContext.Provider value={host}>\n <DirectiveHostProvider host={host.directives ?? {}}>{children}</DirectiveHostProvider>\n </ContentIrHostContext.Provider>\n );\n}\n\n/**\n * The wired host, or null. For the rare consumer that can legitimately run\n * without a provider because it was handed its sources explicitly (see\n * `useContentIrKindVersion`). Everything else uses {@link useContentIrHost}.\n */\nexport function useContentIrHostOrNull(): ContentIrHost | null {\n return useContext(ContentIrHostContext);\n}\n\n/** The wired host. Throws outside a provider — see the module doc. */\nexport function useContentIrHost(): ContentIrHost {\n const host = useContentIrHostOrNull();\n if (!host) {\n throw new Error(\n \"[content-ir-react] No ContentIrRenderProvider above this component. \" +\n \"Mount one at the app root with your host adapter (kind definitions, \" +\n \"component resolver, renderBlock, renderValue, reportError).\",\n );\n }\n return host;\n}\n\n/** The wired host narrowed to what the pure route functions take. */\nexport function useKindRouteEnv(): KindRouteEnv {\n const host = useContentIrHost();\n return useMemo(() => routeEnvOf(host), [host]);\n}\n","\"use client\";\n\n/**\n * THE DIRECTIVE HOST CONTRACT — what the directive tier refuses to decide.\n *\n * The package owns the DECISIONS (decode → renderer → card → floor, the\n * position law, never-null rendering) and the card itself. Four things stay\n * host property and enter through named seams:\n *\n * 1. APPLYING a side-effect directive — `POST /directives/confirm` as the\n * user, with the host's auth and base URL. Absent → the Apply control is\n * ABSENT (honest), never a dead button.\n * 2. OPENING an item to read it before approving — a window panel, a dialog,\n * a route. Absent → items are not clickable.\n * 3. COPY controls — host chrome (matrx-frontend's CopyButtons). Absent → none.\n * 4. The NOUN catalog + item-kind lookup — server-derived tables the host\n * mirrors (or reads from its kind catalog's `items` edges).\n * 5. ASKING (`ask`), NAMING a record (`renderRecord`), and READING the ledger\n * (`applyState`) — the consequence before the click, the doors, and the\n * state that survives a reload.\n *\n * A host that has adopted `ContentIrRenderProvider` passes `directives` on its\n * ContentIrHost; a host that has NOT (Workflow Studio, 2026-09-08) mounts\n * `DirectiveHostProvider` alone — the directive tier never requires the full\n * render host, because a chat bubble must be able to draw a directive card\n * identically everywhere, today.\n */\n\nimport { createContext, useContext, type ReactNode } from \"react\";\nimport type {\n DecodedDirective,\n DirectiveItemKindLookup,\n DirectiveNounCatalog,\n} from \"@ai-matrx/content-ir\";\n\nexport interface DirectiveApplyResult {\n applied: number;\n failed: number;\n /**\n * The server's own receipt sentence (\"Deleted task “Ship it”.\"), shown in\n * place of a bare count. Never composed client-side.\n */\n message?: string | null;\n /**\n * The records the apply created or changed, so the tally is a way IN to them\n * (no dead ends). Rendered through `renderRecord`; absent → the sentence alone.\n */\n records?: DirectiveRecordRef[];\n /**\n * For an UPDATE: what each item's fields held just BEFORE the write, by item\n * index (`null` for an item the server did not record). The card draws the\n * applied change from this record — never from a live re-read, which after the\n * apply answers the NEW value (G6A review, 2026-10-02: \"(no change)\" for a\n * change that happened).\n */\n before?: ReadonlyArray<Record<string, unknown> | null>;\n}\n\n/**\n * What the server's action ledger says about ONE block, read on mount so a\n * reloaded page never offers \"Apply\" for work that already ran (reviewer\n * defect 5, 2026-10-02).\n *\n * - `not_applied` — still to do (or partly done: the batch re-applies, and the\n * server replays the members it already holds). The card offers Apply.\n * - `in_flight` — another request is applying it right now. The card says it\n * is finishing and asks again shortly; it offers neither Apply nor a tally.\n * - `applied` — the ledger holds every item. The card shows the tally exactly\n * as a fresh apply would: the ledger's own sentence and the records, as doors.\n */\nexport interface DirectiveApplyState {\n state: \"not_applied\" | \"in_flight\" | \"applied\";\n /** The ledger row's sentence (\"Created task.\"), never composed client-side. */\n message?: string | null;\n /**\n * The records the LATEST apply wrote — the tally's doors. After \"Run again\"\n * that is the newest copy, never the first.\n */\n records?: DirectiveRecordRef[];\n /**\n * How many times the ledger holds this block as applied: 1 for the original,\n * one more for every \"Run again\". Absent → 1. A create that ran twice made\n * two records, and the card says so (reviewer, 2026-10-02).\n */\n copies?: number;\n /**\n * For an applied UPDATE: each item's replaced values, by item index, from the\n * ledger row (`null` where the ledger has none — a row from before 2026-10-02).\n * Absent → the card says only what each field was set to.\n */\n before?: ReadonlyArray<Record<string, unknown> | null>;\n}\n\n/** One record a directive names: the noun it is, and its id. */\nexport interface DirectiveRecordRef {\n noun: string;\n id: string;\n}\n\nexport interface DirectiveRecordProps extends DirectiveRecordRef {\n /**\n * What to show until (or unless) the host resolves the live name —\n * \"Task 1a2b3c4d\". Never blank, never \"Item\".\n */\n fallback: string;\n /** Where it is drawn: an item row, the confirm dialog, or the applied tally. */\n context: \"row\" | \"dialog\" | \"applied\";\n /**\n * True when the record is (about to be) in the trash — the host may still\n * name it but should not promise it opens as a live record.\n */\n trashed?: boolean;\n}\n\n/** What the person is being asked to approve — everything the card knows. */\nexport interface DirectiveAskRequest {\n directive: DecodedDirective;\n /** The items, exactly as they will be sent. */\n items: Record<string, unknown>[];\n /** The noun's human label (\"Task\"), from the host's catalog when it has one. */\n nounLabel: string;\n /**\n * True for \"Run again\": the ledger already holds this block, and a yes runs\n * it a SECOND time (`force`) — another copy for a create, the same fields\n * written again for an update, another delete for a restored record. The\n * question must say so.\n */\n again?: boolean;\n}\n\n/**\n * The shell a card hands its host — the two keys, plus WHERE it sits.\n *\n * `conversationId` is the conversation the card renders in (a chat message),\n * absent in a person's own content (a note). It is the idempotency NAMESPACE:\n * a card in an agent's message must apply under the SAME key as that\n * proposal's Approve button, or the one action could run twice.\n */\nexport interface DirectiveShell {\n __kind: string;\n items: Record<string, unknown>[];\n conversationId?: string;\n /** \"Run again\": skip the ledger's dedup and apply a second time. */\n force?: boolean;\n}\n\nexport interface DirectiveOpenItemOptions {\n /** The item, `__kind`-stamped when the shape has an item kind. */\n item: Record<string, unknown>;\n /** The content-IR kind this item IS, or null when the shape has none. */\n itemKind: string | null;\n /** What the item is called — the same title the card shows. */\n title: string;\n /** \"Run Create agent definition · item 1 of 1\". */\n subtitle: string | null;\n directive: DecodedDirective;\n index: number;\n total: number;\n}\n\nexport interface DirectiveCopyProps {\n label: string;\n /** The value to copy — the whole shell or one item. */\n value: unknown;\n /** The item's kind, when copying an item. */\n kind?: string | null;\n size: \"icon\" | \"xs\";\n}\n\nexport interface DirectiveHost {\n /**\n * Apply a side-effect directive as the user (the server re-validates every\n * item against the registered model and runs the ONE handler; idempotent).\n * Reject with an Error whose message is safe to show.\n */\n confirm?: (shell: DirectiveShell) => Promise<DirectiveApplyResult>;\n /**\n * Ask the person BEFORE anything runs, naming the consequence (the records,\n * what changes). Resolve `true` to go ahead, `false` to leave the card as it\n * was. The Apply control stays idle while this is open — nothing reads as\n * \"applying\" until the person has said yes. Absent → `confirm` is called\n * directly (a host that asks inside `confirm` keeps working, but its card\n * will read \"Applying…\" while its dialog is up).\n */\n ask?: (request: DirectiveAskRequest) => Promise<boolean>;\n /**\n * Has this exact block already been applied? Read ONCE when the card mounts\n * (and again while the answer is `in_flight`), so the card survives a reload:\n * the server ledger, not component state, is what knows. The identity is the\n * server's (it hashes the validated items in the same namespace `confirm`\n * applies in), so a host asks — it never computes. Batch on the host side if\n * a page carries many cards. Reject with an Error when the ledger cannot be\n * read: the card reports it and offers Apply, which is safe because `confirm`\n * is idempotent. Absent → the card starts at Apply, as before.\n */\n applyState?: (shell: DirectiveShell) => Promise<DirectiveApplyState>;\n /**\n * Read the CURRENT values of ONE existing record, so an update says what it\n * replaces — \"Status open → done\", never just \"→ done\" (reviewer,\n * 2026-10-02). Resolve `null` when the record cannot be read (no access, no\n * such record, a noun the host cannot read); reject only for a real failure.\n * Absent → an update row shows the new values alone, as before.\n */\n readRecord?: (ref: DirectiveRecordRef) => Promise<Record<string, unknown> | null>;\n /**\n * Draw ONE existing record by its live name, as a way to open it. Used on\n * update/delete rows (the record that will change), in the host's own ask\n * dialog if it likes, and on the applied tally (what was written). Absent →\n * the fallback text, unlinked.\n */\n renderRecord?: (props: DirectiveRecordProps) => ReactNode;\n /** Open ONE item to read it properly before approving the write. */\n openItem?: (options: DirectiveOpenItemOptions) => void;\n /** Host copy controls. */\n renderCopy?: (props: DirectiveCopyProps) => ReactNode;\n /** The mirrored noun catalog (label / family / title column). */\n nouns?: DirectiveNounCatalog;\n /** slug → the kind ONE item is (THE DIRECTIVE⇄KIND SEAM), or null. */\n itemKind?: DirectiveItemKindLookup;\n /** Where a decode failure or an unhonourable shell is reported. Default: console.error. */\n reportError?: (message: string) => void;\n /**\n * Plain words for a failed apply the host recognises (\"This button can't\n * create a task yet.\" + \"Create it from Tasks instead.\"). The card never shows\n * the server's raw text as its sentence: it shows these words — or its own\n * verb-aware default — and keeps the raw text behind \"Details\". Return null\n * to use the default.\n */\n explainFailure?: (failure: DirectiveFailure) => DirectiveFailureWords | null;\n}\n\n/** One failed apply, as the card knows it. */\nexport interface DirectiveFailure {\n /** The server's / host's own text — technical; shown only behind Details. */\n raw: string;\n directiveClass: string;\n noun: string;\n /** The noun's display label (\"Task\"). */\n nounLabel: string;\n /** Items that failed, when some applied and some did not. */\n failed?: number;\n /** Items in the block. */\n total?: number;\n}\n\n/** What failed and what to do — each one short sentence. */\nexport interface DirectiveFailureWords {\n what: string;\n next: string;\n}\n\nconst DirectiveHostContext = createContext<DirectiveHost | null>(null);\n\n/** Where a directive renders — today, the conversation it sits in (if any). */\nexport interface DirectiveScope {\n conversationId?: string | null;\n}\n\nconst DirectiveScopeContext = createContext<DirectiveScope>({});\n\n/**\n * Tell every card below WHERE it renders. A host draws a chat message's\n * directives inside one of these (or passes `conversationId` to\n * `DirectiveRender`), so the card applies in that conversation's namespace —\n * the same key the agent's proposal approves under. A note mounts none.\n */\nexport function DirectiveScopeProvider({\n conversationId,\n children,\n}: DirectiveScope & { children: ReactNode }) {\n return (\n <DirectiveScopeContext.Provider value={{ conversationId: conversationId ?? null }}>\n {children}\n </DirectiveScopeContext.Provider>\n );\n}\n\nexport function useDirectiveScope(): DirectiveScope {\n return useContext(DirectiveScopeContext);\n}\n\nexport interface DirectiveHostProviderProps {\n host: DirectiveHost;\n children: ReactNode;\n}\n\nexport function DirectiveHostProvider({ host, children }: DirectiveHostProviderProps) {\n return <DirectiveHostContext.Provider value={host}>{children}</DirectiveHostContext.Provider>;\n}\n\nconst EMPTY_HOST: DirectiveHost = {};\n\n/**\n * The wired directive host. Unlike the render host this does NOT throw when\n * unwired: a directive still renders (named, itemised, copy-less, apply-less)\n * — degrading the chrome is honest, hiding the content is not.\n */\nexport function useDirectiveHost(): DirectiveHost {\n return useContext(DirectiveHostContext) ?? EMPTY_HOST;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC2BO,IAAM,uBAA+C,CAAC,WAAW;AACtE,UAAQ,MAAM,gBAAgB,OAAO,OAAO,IAAI,OAAO,OAAO,EAAE;AAClE;;;AC0HO,SAAS,WAAW,MAAmC;AAC5D,SAAO;AAAA,IACL,OAAO,KAAK;AAAA,IACZ,YAAY,KAAK;AAAA,IACjB,aAAa,KAAK;AAAA,IAClB,UAAU,KAAK;AAAA,EACjB;AACF;;;AC1IA,IAAAA,gBAAmE;;;ACQnE,mBAA0D;AAmPtD;AApBJ,IAAM,2BAAuB,4BAAoC,IAAI;AAOrE,IAAM,4BAAwB,4BAA8B,CAAC,CAAC;AA4BvD,SAAS,sBAAsB,EAAE,MAAM,SAAS,GAA+B;AACpF,SAAO,4CAAC,qBAAqB,UAArB,EAA8B,OAAO,MAAO,UAAS;AAC/D;;;AD1PM,IAAAC,sBAAA;AAbN,IAAM,2BAAuB,6BAAoC,IAAI;AAO9D,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,GAAiC;AAC/B,SACE,6CAAC,qBAAqB,UAArB,EAA8B,OAAO,MACpC,uDAAC,yBAAsB,MAAM,KAAK,cAAc,CAAC,GAAI,UAAS,GAChE;AAEJ;AAOO,SAAS,yBAA+C;AAC7D,aAAO,0BAAW,oBAAoB;AACxC;AAGO,SAAS,mBAAkC;AAChD,QAAM,OAAO,uBAAuB;AACpC,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,kBAAgC;AAC9C,QAAM,OAAO,iBAAiB;AAC9B,aAAO,uBAAQ,MAAM,WAAW,IAAI,GAAG,CAAC,IAAI,CAAC;AAC/C;","names":["import_react","import_jsx_runtime"]}
package/dist/host.d.cts CHANGED
@@ -1,10 +1,10 @@
1
1
  export { b as ContentIrErrorReport, a as ContentIrErrorReporter, c as consoleErrorReporter } from './component-resolution.types-DwKSF64x.cjs';
2
- import { C as ContentIrHost, K as KindRouteEnv } from './host-types-CKu4ap5K.cjs';
3
- export { a as ContentIrRenderSeams, I as IrRenderBlock, b as IrRoutableBlock, c as KindDefinitionSource, S as StructuredValueRenderProps, r as routeEnvOf } from './host-types-CKu4ap5K.cjs';
2
+ import { C as ContentIrHost, K as KindRouteEnv } from './host-types-YP_l_Q_t.cjs';
3
+ export { a as ContentIrRenderSeams, I as IrRenderBlock, b as IrRoutableBlock, c as KindDefinitionSource, S as StructuredValueRenderProps, r as routeEnvOf } from './host-types-YP_l_Q_t.cjs';
4
4
  import * as react from 'react';
5
5
  import { ReactNode } from 'react';
6
6
  import '@ai-matrx/content-ir';
7
- import './directive-host-BOML4BQ7.cjs';
7
+ import './directive-host-Bw2_PrgL.cjs';
8
8
 
9
9
  interface ContentIrRenderProviderProps {
10
10
  host: ContentIrHost;
package/dist/host.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export { b as ContentIrErrorReport, a as ContentIrErrorReporter, c as consoleErrorReporter } from './component-resolution.types-DwKSF64x.js';
2
- import { C as ContentIrHost, K as KindRouteEnv } from './host-types-CQ2oIzUy.js';
3
- export { a as ContentIrRenderSeams, I as IrRenderBlock, b as IrRoutableBlock, c as KindDefinitionSource, S as StructuredValueRenderProps, r as routeEnvOf } from './host-types-CQ2oIzUy.js';
2
+ import { C as ContentIrHost, K as KindRouteEnv } from './host-types-eSjFmXyF.js';
3
+ export { a as ContentIrRenderSeams, I as IrRenderBlock, b as IrRoutableBlock, c as KindDefinitionSource, S as StructuredValueRenderProps, r as routeEnvOf } from './host-types-eSjFmXyF.js';
4
4
  import * as react from 'react';
5
5
  import { ReactNode } from 'react';
6
6
  import '@ai-matrx/content-ir';
7
- import './directive-host-BOML4BQ7.js';
7
+ import './directive-host-Bw2_PrgL.js';
8
8
 
9
9
  interface ContentIrRenderProviderProps {
10
10
  host: ContentIrHost;
package/dist/host.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../host/error-report.ts","../host/host-types.ts","../host/ContentIrProvider.tsx","../directives/directive-host.tsx"],"sourcesContent":["/**\n * THE SCREAM SEAM.\n *\n * Every recovery path in this package is loud: a defective component row, a\n * failed resolver load, a kind whose component throws on a provisional value.\n * The package refuses to own where those screams land — matrx-frontend routes\n * them to the Error Inspector, the dashboard to its own log, the extension to\n * its background page. Each host binds one function.\n *\n * A host that binds a no-op has chosen silence, which is a defect in itself;\n * {@link consoleErrorReporter} is the honest default.\n */\n\nexport interface ContentIrErrorReport {\n /** Always `\"content-ir\"` — hosts key their capture stores on it. */\n source: \"content-ir\";\n message: string;\n name?: string;\n stack?: string;\n /** Free-form grouping key (the host's `relation`), e.g. `\"partial-kind\"`. */\n relation?: string;\n raw?: unknown;\n}\n\nexport type ContentIrErrorReporter = (report: ContentIrErrorReport) => void;\n\n/** The default when a host binds nothing: never silent. */\nexport const consoleErrorReporter: ContentIrErrorReporter = (report) => {\n console.error(`[content-ir] ${report.message}`, report.raw ?? \"\");\n};\n","/**\n * THE HOST CONTRACT — everything this package refuses to decide.\n *\n * The kernel (`@ai-matrx/content-ir`) proved the shape: extract the portable\n * part, let host capability enter through explicit named seams. The render\n * layer needs four things it must never own:\n *\n * 1. WHERE KIND DEFINITIONS COME FROM (a bundled table, `content_ir`, a REST\n * call). The route only asks \"is this kind registered, and does it carry a\n * legacy bridge / a partial-ready opt-in\".\n * 2. WHERE COMPONENT ROWS COME FROM — {@link ComponentResolutionSource}. The\n * package ships `ComponentResolver`, which any host can construct with its\n * own loaders; a host with its own resolver only has to satisfy the type.\n * 3. HOW A ROUTED BLOCK ACTUALLY DRAWS. `applyIrKindRoute` decides a block's\n * TYPE; turning that type into pixels is the host's dispatch table\n * (matrx-frontend's `BlockRenderer`, the dashboard's small map). The\n * package never imports a component library.\n * 4. WHERE SCREAMS LAND — {@link ContentIrErrorReporter}.\n *\n * No Next.js, no Redux, no Supabase, no router, no host error capture. If a\n * seam is missing, ADD A SEAM — never an import.\n */\n\nimport type { ReactNode } from \"react\";\nimport type { KindDefinition } from \"@ai-matrx/content-ir\";\nimport type { ContentIrErrorReporter } from \"./error-report\";\nimport type { ComponentResolutionSource } from \"../resolver/component-resolution.types\";\nimport type { DirectiveHost } from \"../directives/directive-host\";\n\n/**\n * The minimum a render block must expose for the kind route to work. Hosts\n * pass their own richer block type through; the route is generic over `T` and\n * preserves every extra field.\n */\nexport interface IrRoutableBlock {\n type: string;\n serverData?: Record<string, unknown>;\n metadata?: Record<string, unknown>;\n}\n\n/** A routable block that also carries its source text (the raw region). */\nexport interface IrRenderBlock extends IrRoutableBlock {\n content: string;\n language?: string;\n}\n\n/**\n * Where registered kinds come from. `getDefinition` is the only method the\n * pure route needs; the rest drive the granular repaint and the warm-up tick\n * that keeps a cold registry from being mistaken for \"this kind has no\n * component\".\n */\nexport interface KindDefinitionSource {\n getDefinition(kind: string): KindDefinition | undefined;\n /** Monotonic per-kind version — the repaint snapshot key. */\n getKindVersion(kind: string): number;\n subscribeKind(kind: string, listener: () => void): () => void;\n /** One definition load per app session. */\n ensureWarm(): Promise<void>;\n}\n\n/**\n * The rendering seams. Everything here returns host UI; the package supplies\n * the DECISIONS and the chrome-free structure around them.\n */\nexport interface ContentIrRenderSeams {\n /**\n * Draw a block the kind route has already typed. This is the host's dispatch\n * table — the single place a component key becomes a component.\n */\n renderBlock(block: IrRenderBlock): ReactNode;\n /**\n * THE FLOOR: render ANY JSON value as a human document. Reached when a kind\n * has no render-trusted component, when the value is not an object at all,\n * and by the generic structured view.\n *\n * This is deliberately a seam and not a bundled component. Rendering a value\n * WELL means prose through the host's markdown renderer, media through the\n * host's file handler, and uniform arrays through the host's data table —\n * all of which are host property. A host that has none of those can pass a\n * `<pre>`; it will be honest, just plain.\n */\n renderValue(props: StructuredValueRenderProps): ReactNode;\n /**\n * The \"still arriving\" indicator used by the provisional frame. Optional —\n * hosts without a shimmer get plain text.\n */\n renderShimmer?(text: string): ReactNode;\n /**\n * The honest \"this shape has no custom component yet\" notice. Optional: the\n * package draws a plain amber line when a host supplies nothing, and a host\n * with an icon set or its own callout component supplies that instead. It is\n * a seam and not a bundled component because this package depends on no icon\n * library.\n */\n renderNotice?(text: string): ReactNode;\n}\n\nexport interface StructuredValueRenderProps {\n value: unknown;\n /** The kind slug this value claims, when known. Honesty line only — never a renderer choice. */\n kind?: string;\n /** Why this shape has no custom view, in human words. */\n note?: string;\n /** Show the \"what this is / raw data\" footer. Default true. */\n footer?: boolean;\n}\n\n/**\n * One host, wired once, read by every component in this package through\n * {@link ContentIrRenderProvider}.\n */\nexport interface ContentIrHost extends ContentIrRenderSeams {\n kinds: KindDefinitionSource;\n components: ComponentResolutionSource;\n reportError: ContentIrErrorReporter;\n /**\n * The `kind_component.platform` this host resolves as. \"web\" for every\n * browser UI; the column already models \"react-native\" and friends, and a\n * host that lies here renders the wrong component everywhere.\n */\n platform: string;\n /**\n * The Kind Directives seams (apply / open item / copy / noun catalog / item\n * kind). Optional: a host without them still renders every directive card,\n * apply-less and copy-less — honest chrome, never hidden content. Provided\n * to the directive tier by `ContentIrRenderProvider`; a host that has not\n * adopted the render provider mounts `DirectiveHostProvider` directly.\n */\n directives?: DirectiveHost;\n}\n\n/**\n * The pure route's dependencies — a strict subset of {@link ContentIrHost},\n * because `applyIrKindRoute` is called OUTSIDE React (reducers, stream\n * accumulators, tests) where there is no provider to read.\n */\nexport interface KindRouteEnv {\n kinds: Pick<KindDefinitionSource, \"getDefinition\">;\n /**\n * `hasSettled` is optional and read ONLY to suppress a negative verdict from\n * a registry that has not loaded yet — see THE COLD-VERDICT RULE on\n * `ComponentResolutionSource`.\n */\n components: Pick<ComponentResolutionSource, \"resolve\"> &\n Partial<Pick<ComponentResolutionSource, \"hasSettled\">>;\n reportError: ContentIrErrorReporter;\n platform: string;\n}\n\n/** Narrow a host (or anything host-shaped) to what the pure route needs. */\nexport function routeEnvOf(host: ContentIrHost): KindRouteEnv {\n return {\n kinds: host.kinds,\n components: host.components,\n reportError: host.reportError,\n platform: host.platform,\n };\n}\n","\"use client\";\n\n/**\n * The one place a host is wired in. Everything React in this package reads its\n * capability from here — no module singleton, no import into an app.\n *\n * A host mounts this ONCE, high enough that every kind-rendering surface is\n * inside it:\n *\n * ```tsx\n * <ContentIrRenderProvider host={matrxContentIrHost}>\n * <App />\n * </ContentIrRenderProvider>\n * ```\n *\n * Reading it outside a provider THROWS rather than falling back to a default.\n * A silent default would render the wrong component for every kind in the app\n * and look like a data problem for weeks.\n */\n\nimport { createContext, useContext, useMemo, type ReactNode } from \"react\";\nimport type { ContentIrHost, KindRouteEnv } from \"./host-types\";\nimport { routeEnvOf } from \"./host-types\";\nimport { DirectiveHostProvider } from \"../directives/directive-host\";\n\nconst ContentIrHostContext = createContext<ContentIrHost | null>(null);\n\nexport interface ContentIrRenderProviderProps {\n host: ContentIrHost;\n children: ReactNode;\n}\n\nexport function ContentIrRenderProvider({\n host,\n children,\n}: ContentIrRenderProviderProps) {\n return (\n <ContentIrHostContext.Provider value={host}>\n <DirectiveHostProvider host={host.directives ?? {}}>{children}</DirectiveHostProvider>\n </ContentIrHostContext.Provider>\n );\n}\n\n/**\n * The wired host, or null. For the rare consumer that can legitimately run\n * without a provider because it was handed its sources explicitly (see\n * `useContentIrKindVersion`). Everything else uses {@link useContentIrHost}.\n */\nexport function useContentIrHostOrNull(): ContentIrHost | null {\n return useContext(ContentIrHostContext);\n}\n\n/** The wired host. Throws outside a provider — see the module doc. */\nexport function useContentIrHost(): ContentIrHost {\n const host = useContentIrHostOrNull();\n if (!host) {\n throw new Error(\n \"[content-ir-react] No ContentIrRenderProvider above this component. \" +\n \"Mount one at the app root with your host adapter (kind definitions, \" +\n \"component resolver, renderBlock, renderValue, reportError).\",\n );\n }\n return host;\n}\n\n/** The wired host narrowed to what the pure route functions take. */\nexport function useKindRouteEnv(): KindRouteEnv {\n const host = useContentIrHost();\n return useMemo(() => routeEnvOf(host), [host]);\n}\n","\"use client\";\n\n/**\n * THE DIRECTIVE HOST CONTRACT — what the directive tier refuses to decide.\n *\n * The package owns the DECISIONS (decode → renderer → card → floor, the\n * position law, never-null rendering) and the card itself. Four things stay\n * host property and enter through named seams:\n *\n * 1. APPLYING a side-effect directive — `POST /directives/confirm` as the\n * user, with the host's auth and base URL. Absent → the Apply control is\n * ABSENT (honest), never a dead button.\n * 2. OPENING an item to read it before approving — a window panel, a dialog,\n * a route. Absent → items are not clickable.\n * 3. COPY controls — host chrome (matrx-frontend's CopyButtons). Absent → none.\n * 4. The NOUN catalog + item-kind lookup — server-derived tables the host\n * mirrors (or reads from its kind catalog's `items` edges).\n * 5. ASKING (`ask`), NAMING a record (`renderRecord`), and READING the ledger\n * (`applyState`) — the consequence before the click, the doors, and the\n * state that survives a reload.\n *\n * A host that has adopted `ContentIrRenderProvider` passes `directives` on its\n * ContentIrHost; a host that has NOT (Workflow Studio, 2026-09-08) mounts\n * `DirectiveHostProvider` alone — the directive tier never requires the full\n * render host, because a chat bubble must be able to draw a directive card\n * identically everywhere, today.\n */\n\nimport { createContext, useContext, type ReactNode } from \"react\";\nimport type {\n DecodedDirective,\n DirectiveItemKindLookup,\n DirectiveNounCatalog,\n} from \"@ai-matrx/content-ir\";\n\nexport interface DirectiveApplyResult {\n applied: number;\n failed: number;\n /**\n * The server's own receipt sentence (\"Deleted task “Ship it”.\"), shown in\n * place of a bare count. Never composed client-side.\n */\n message?: string | null;\n /**\n * The records the apply created or changed, so the tally is a way IN to them\n * (no dead ends). Rendered through `renderRecord`; absent → the sentence alone.\n */\n records?: DirectiveRecordRef[];\n}\n\n/**\n * What the server's action ledger says about ONE block, read on mount so a\n * reloaded page never offers \"Apply\" for work that already ran (reviewer\n * defect 5, 2026-10-02).\n *\n * - `not_applied` — still to do (or partly done: the batch re-applies, and the\n * server replays the members it already holds). The card offers Apply.\n * - `in_flight` — another request is applying it right now. The card says it\n * is finishing and asks again shortly; it offers neither Apply nor a tally.\n * - `applied` — the ledger holds every item. The card shows the tally exactly\n * as a fresh apply would: the ledger's own sentence and the records, as doors.\n */\nexport interface DirectiveApplyState {\n state: \"not_applied\" | \"in_flight\" | \"applied\";\n /** The ledger row's sentence (\"Created task.\"), never composed client-side. */\n message?: string | null;\n /**\n * The records the LATEST apply wrote — the tally's doors. After \"Run again\"\n * that is the newest copy, never the first.\n */\n records?: DirectiveRecordRef[];\n /**\n * How many times the ledger holds this block as applied: 1 for the original,\n * one more for every \"Run again\". Absent → 1. A create that ran twice made\n * two records, and the card says so (reviewer, 2026-10-02).\n */\n copies?: number;\n}\n\n/** One record a directive names: the noun it is, and its id. */\nexport interface DirectiveRecordRef {\n noun: string;\n id: string;\n}\n\nexport interface DirectiveRecordProps extends DirectiveRecordRef {\n /**\n * What to show until (or unless) the host resolves the live name —\n * \"Task 1a2b3c4d\". Never blank, never \"Item\".\n */\n fallback: string;\n /** Where it is drawn: an item row, the confirm dialog, or the applied tally. */\n context: \"row\" | \"dialog\" | \"applied\";\n /**\n * True when the record is (about to be) in the trash — the host may still\n * name it but should not promise it opens as a live record.\n */\n trashed?: boolean;\n}\n\n/** What the person is being asked to approve — everything the card knows. */\nexport interface DirectiveAskRequest {\n directive: DecodedDirective;\n /** The items, exactly as they will be sent. */\n items: Record<string, unknown>[];\n /** The noun's human label (\"Task\"), from the host's catalog when it has one. */\n nounLabel: string;\n /**\n * True for \"Run again\": the ledger already holds this block, and a yes runs\n * it a SECOND time (`force`) — another copy for a create, the same fields\n * written again for an update, another delete for a restored record. The\n * question must say so.\n */\n again?: boolean;\n}\n\n/**\n * The shell a card hands its host — the two keys, plus WHERE it sits.\n *\n * `conversationId` is the conversation the card renders in (a chat message),\n * absent in a person's own content (a note). It is the idempotency NAMESPACE:\n * a card in an agent's message must apply under the SAME key as that\n * proposal's Approve button, or the one action could run twice.\n */\nexport interface DirectiveShell {\n __kind: string;\n items: Record<string, unknown>[];\n conversationId?: string;\n /** \"Run again\": skip the ledger's dedup and apply a second time. */\n force?: boolean;\n}\n\nexport interface DirectiveOpenItemOptions {\n /** The item, `__kind`-stamped when the shape has an item kind. */\n item: Record<string, unknown>;\n /** The content-IR kind this item IS, or null when the shape has none. */\n itemKind: string | null;\n /** What the item is called — the same title the card shows. */\n title: string;\n /** \"Run Create agent definition · item 1 of 1\". */\n subtitle: string | null;\n directive: DecodedDirective;\n index: number;\n total: number;\n}\n\nexport interface DirectiveCopyProps {\n label: string;\n /** The value to copy — the whole shell or one item. */\n value: unknown;\n /** The item's kind, when copying an item. */\n kind?: string | null;\n size: \"icon\" | \"xs\";\n}\n\nexport interface DirectiveHost {\n /**\n * Apply a side-effect directive as the user (the server re-validates every\n * item against the registered model and runs the ONE handler; idempotent).\n * Reject with an Error whose message is safe to show.\n */\n confirm?: (shell: DirectiveShell) => Promise<DirectiveApplyResult>;\n /**\n * Ask the person BEFORE anything runs, naming the consequence (the records,\n * what changes). Resolve `true` to go ahead, `false` to leave the card as it\n * was. The Apply control stays idle while this is open — nothing reads as\n * \"applying\" until the person has said yes. Absent → `confirm` is called\n * directly (a host that asks inside `confirm` keeps working, but its card\n * will read \"Applying…\" while its dialog is up).\n */\n ask?: (request: DirectiveAskRequest) => Promise<boolean>;\n /**\n * Has this exact block already been applied? Read ONCE when the card mounts\n * (and again while the answer is `in_flight`), so the card survives a reload:\n * the server ledger, not component state, is what knows. The identity is the\n * server's (it hashes the validated items in the same namespace `confirm`\n * applies in), so a host asks — it never computes. Batch on the host side if\n * a page carries many cards. Reject with an Error when the ledger cannot be\n * read: the card reports it and offers Apply, which is safe because `confirm`\n * is idempotent. Absent → the card starts at Apply, as before.\n */\n applyState?: (shell: DirectiveShell) => Promise<DirectiveApplyState>;\n /**\n * Read the CURRENT values of ONE existing record, so an update says what it\n * replaces — \"Status open → done\", never just \"→ done\" (reviewer,\n * 2026-10-02). Resolve `null` when the record cannot be read (no access, no\n * such record, a noun the host cannot read); reject only for a real failure.\n * Absent → an update row shows the new values alone, as before.\n */\n readRecord?: (ref: DirectiveRecordRef) => Promise<Record<string, unknown> | null>;\n /**\n * Draw ONE existing record by its live name, as a way to open it. Used on\n * update/delete rows (the record that will change), in the host's own ask\n * dialog if it likes, and on the applied tally (what was written). Absent →\n * the fallback text, unlinked.\n */\n renderRecord?: (props: DirectiveRecordProps) => ReactNode;\n /** Open ONE item to read it properly before approving the write. */\n openItem?: (options: DirectiveOpenItemOptions) => void;\n /** Host copy controls. */\n renderCopy?: (props: DirectiveCopyProps) => ReactNode;\n /** The mirrored noun catalog (label / family / title column). */\n nouns?: DirectiveNounCatalog;\n /** slug → the kind ONE item is (THE DIRECTIVE⇄KIND SEAM), or null. */\n itemKind?: DirectiveItemKindLookup;\n /** Where a decode failure or an unhonourable shell is reported. Default: console.error. */\n reportError?: (message: string) => void;\n /**\n * Plain words for a failed apply the host recognises (\"This button can't\n * create a task yet.\" + \"Create it from Tasks instead.\"). The card never shows\n * the server's raw text as its sentence: it shows these words — or its own\n * verb-aware default — and keeps the raw text behind \"Details\". Return null\n * to use the default.\n */\n explainFailure?: (failure: DirectiveFailure) => DirectiveFailureWords | null;\n}\n\n/** One failed apply, as the card knows it. */\nexport interface DirectiveFailure {\n /** The server's / host's own text — technical; shown only behind Details. */\n raw: string;\n directiveClass: string;\n noun: string;\n /** The noun's display label (\"Task\"). */\n nounLabel: string;\n /** Items that failed, when some applied and some did not. */\n failed?: number;\n /** Items in the block. */\n total?: number;\n}\n\n/** What failed and what to do — each one short sentence. */\nexport interface DirectiveFailureWords {\n what: string;\n next: string;\n}\n\nconst DirectiveHostContext = createContext<DirectiveHost | null>(null);\n\n/** Where a directive renders — today, the conversation it sits in (if any). */\nexport interface DirectiveScope {\n conversationId?: string | null;\n}\n\nconst DirectiveScopeContext = createContext<DirectiveScope>({});\n\n/**\n * Tell every card below WHERE it renders. A host draws a chat message's\n * directives inside one of these (or passes `conversationId` to\n * `DirectiveRender`), so the card applies in that conversation's namespace —\n * the same key the agent's proposal approves under. A note mounts none.\n */\nexport function DirectiveScopeProvider({\n conversationId,\n children,\n}: DirectiveScope & { children: ReactNode }) {\n return (\n <DirectiveScopeContext.Provider value={{ conversationId: conversationId ?? null }}>\n {children}\n </DirectiveScopeContext.Provider>\n );\n}\n\nexport function useDirectiveScope(): DirectiveScope {\n return useContext(DirectiveScopeContext);\n}\n\nexport interface DirectiveHostProviderProps {\n host: DirectiveHost;\n children: ReactNode;\n}\n\nexport function DirectiveHostProvider({ host, children }: DirectiveHostProviderProps) {\n return <DirectiveHostContext.Provider value={host}>{children}</DirectiveHostContext.Provider>;\n}\n\nconst EMPTY_HOST: DirectiveHost = {};\n\n/**\n * The wired directive host. Unlike the render host this does NOT throw when\n * unwired: a directive still renders (named, itemised, copy-less, apply-less)\n * — degrading the chrome is honest, hiding the content is not.\n */\nexport function useDirectiveHost(): DirectiveHost {\n return useContext(DirectiveHostContext) ?? EMPTY_HOST;\n}\n"],"mappings":";;;AA2BO,IAAM,uBAA+C,CAAC,WAAW;AACtE,UAAQ,MAAM,gBAAgB,OAAO,OAAO,IAAI,OAAO,OAAO,EAAE;AAClE;;;AC0HO,SAAS,WAAW,MAAmC;AAC5D,SAAO;AAAA,IACL,OAAO,KAAK;AAAA,IACZ,YAAY,KAAK;AAAA,IACjB,aAAa,KAAK;AAAA,IAClB,UAAU,KAAK;AAAA,EACjB;AACF;;;AC1IA,SAAS,iBAAAA,gBAAe,cAAAC,aAAY,eAA+B;;;ACQnE,SAAS,eAAe,kBAAkC;AAqOtD;AApBJ,IAAM,uBAAuB,cAAoC,IAAI;AAOrE,IAAM,wBAAwB,cAA8B,CAAC,CAAC;AA4BvD,SAAS,sBAAsB,EAAE,MAAM,SAAS,GAA+B;AACpF,SAAO,oBAAC,qBAAqB,UAArB,EAA8B,OAAO,MAAO,UAAS;AAC/D;;;AD5OM,gBAAAC,YAAA;AAbN,IAAM,uBAAuBC,eAAoC,IAAI;AAO9D,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,GAAiC;AAC/B,SACE,gBAAAD,KAAC,qBAAqB,UAArB,EAA8B,OAAO,MACpC,0BAAAA,KAAC,yBAAsB,MAAM,KAAK,cAAc,CAAC,GAAI,UAAS,GAChE;AAEJ;AAOO,SAAS,yBAA+C;AAC7D,SAAOE,YAAW,oBAAoB;AACxC;AAGO,SAAS,mBAAkC;AAChD,QAAM,OAAO,uBAAuB;AACpC,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,kBAAgC;AAC9C,QAAM,OAAO,iBAAiB;AAC9B,SAAO,QAAQ,MAAM,WAAW,IAAI,GAAG,CAAC,IAAI,CAAC;AAC/C;","names":["createContext","useContext","jsx","createContext","useContext"]}
1
+ {"version":3,"sources":["../host/error-report.ts","../host/host-types.ts","../host/ContentIrProvider.tsx","../directives/directive-host.tsx"],"sourcesContent":["/**\n * THE SCREAM SEAM.\n *\n * Every recovery path in this package is loud: a defective component row, a\n * failed resolver load, a kind whose component throws on a provisional value.\n * The package refuses to own where those screams land — matrx-frontend routes\n * them to the Error Inspector, the dashboard to its own log, the extension to\n * its background page. Each host binds one function.\n *\n * A host that binds a no-op has chosen silence, which is a defect in itself;\n * {@link consoleErrorReporter} is the honest default.\n */\n\nexport interface ContentIrErrorReport {\n /** Always `\"content-ir\"` — hosts key their capture stores on it. */\n source: \"content-ir\";\n message: string;\n name?: string;\n stack?: string;\n /** Free-form grouping key (the host's `relation`), e.g. `\"partial-kind\"`. */\n relation?: string;\n raw?: unknown;\n}\n\nexport type ContentIrErrorReporter = (report: ContentIrErrorReport) => void;\n\n/** The default when a host binds nothing: never silent. */\nexport const consoleErrorReporter: ContentIrErrorReporter = (report) => {\n console.error(`[content-ir] ${report.message}`, report.raw ?? \"\");\n};\n","/**\n * THE HOST CONTRACT — everything this package refuses to decide.\n *\n * The kernel (`@ai-matrx/content-ir`) proved the shape: extract the portable\n * part, let host capability enter through explicit named seams. The render\n * layer needs four things it must never own:\n *\n * 1. WHERE KIND DEFINITIONS COME FROM (a bundled table, `content_ir`, a REST\n * call). The route only asks \"is this kind registered, and does it carry a\n * legacy bridge / a partial-ready opt-in\".\n * 2. WHERE COMPONENT ROWS COME FROM — {@link ComponentResolutionSource}. The\n * package ships `ComponentResolver`, which any host can construct with its\n * own loaders; a host with its own resolver only has to satisfy the type.\n * 3. HOW A ROUTED BLOCK ACTUALLY DRAWS. `applyIrKindRoute` decides a block's\n * TYPE; turning that type into pixels is the host's dispatch table\n * (matrx-frontend's `BlockRenderer`, the dashboard's small map). The\n * package never imports a component library.\n * 4. WHERE SCREAMS LAND — {@link ContentIrErrorReporter}.\n *\n * No Next.js, no Redux, no Supabase, no router, no host error capture. If a\n * seam is missing, ADD A SEAM — never an import.\n */\n\nimport type { ReactNode } from \"react\";\nimport type { KindDefinition } from \"@ai-matrx/content-ir\";\nimport type { ContentIrErrorReporter } from \"./error-report\";\nimport type { ComponentResolutionSource } from \"../resolver/component-resolution.types\";\nimport type { DirectiveHost } from \"../directives/directive-host\";\n\n/**\n * The minimum a render block must expose for the kind route to work. Hosts\n * pass their own richer block type through; the route is generic over `T` and\n * preserves every extra field.\n */\nexport interface IrRoutableBlock {\n type: string;\n serverData?: Record<string, unknown>;\n metadata?: Record<string, unknown>;\n}\n\n/** A routable block that also carries its source text (the raw region). */\nexport interface IrRenderBlock extends IrRoutableBlock {\n content: string;\n language?: string;\n}\n\n/**\n * Where registered kinds come from. `getDefinition` is the only method the\n * pure route needs; the rest drive the granular repaint and the warm-up tick\n * that keeps a cold registry from being mistaken for \"this kind has no\n * component\".\n */\nexport interface KindDefinitionSource {\n getDefinition(kind: string): KindDefinition | undefined;\n /** Monotonic per-kind version — the repaint snapshot key. */\n getKindVersion(kind: string): number;\n subscribeKind(kind: string, listener: () => void): () => void;\n /** One definition load per app session. */\n ensureWarm(): Promise<void>;\n}\n\n/**\n * The rendering seams. Everything here returns host UI; the package supplies\n * the DECISIONS and the chrome-free structure around them.\n */\nexport interface ContentIrRenderSeams {\n /**\n * Draw a block the kind route has already typed. This is the host's dispatch\n * table — the single place a component key becomes a component.\n */\n renderBlock(block: IrRenderBlock): ReactNode;\n /**\n * THE FLOOR: render ANY JSON value as a human document. Reached when a kind\n * has no render-trusted component, when the value is not an object at all,\n * and by the generic structured view.\n *\n * This is deliberately a seam and not a bundled component. Rendering a value\n * WELL means prose through the host's markdown renderer, media through the\n * host's file handler, and uniform arrays through the host's data table —\n * all of which are host property. A host that has none of those can pass a\n * `<pre>`; it will be honest, just plain.\n */\n renderValue(props: StructuredValueRenderProps): ReactNode;\n /**\n * The \"still arriving\" indicator used by the provisional frame. Optional —\n * hosts without a shimmer get plain text.\n */\n renderShimmer?(text: string): ReactNode;\n /**\n * The honest \"this shape has no custom component yet\" notice. Optional: the\n * package draws a plain amber line when a host supplies nothing, and a host\n * with an icon set or its own callout component supplies that instead. It is\n * a seam and not a bundled component because this package depends on no icon\n * library.\n */\n renderNotice?(text: string): ReactNode;\n}\n\nexport interface StructuredValueRenderProps {\n value: unknown;\n /** The kind slug this value claims, when known. Honesty line only — never a renderer choice. */\n kind?: string;\n /** Why this shape has no custom view, in human words. */\n note?: string;\n /** Show the \"what this is / raw data\" footer. Default true. */\n footer?: boolean;\n}\n\n/**\n * One host, wired once, read by every component in this package through\n * {@link ContentIrRenderProvider}.\n */\nexport interface ContentIrHost extends ContentIrRenderSeams {\n kinds: KindDefinitionSource;\n components: ComponentResolutionSource;\n reportError: ContentIrErrorReporter;\n /**\n * The `kind_component.platform` this host resolves as. \"web\" for every\n * browser UI; the column already models \"react-native\" and friends, and a\n * host that lies here renders the wrong component everywhere.\n */\n platform: string;\n /**\n * The Kind Directives seams (apply / open item / copy / noun catalog / item\n * kind). Optional: a host without them still renders every directive card,\n * apply-less and copy-less — honest chrome, never hidden content. Provided\n * to the directive tier by `ContentIrRenderProvider`; a host that has not\n * adopted the render provider mounts `DirectiveHostProvider` directly.\n */\n directives?: DirectiveHost;\n}\n\n/**\n * The pure route's dependencies — a strict subset of {@link ContentIrHost},\n * because `applyIrKindRoute` is called OUTSIDE React (reducers, stream\n * accumulators, tests) where there is no provider to read.\n */\nexport interface KindRouteEnv {\n kinds: Pick<KindDefinitionSource, \"getDefinition\">;\n /**\n * `hasSettled` is optional and read ONLY to suppress a negative verdict from\n * a registry that has not loaded yet — see THE COLD-VERDICT RULE on\n * `ComponentResolutionSource`.\n */\n components: Pick<ComponentResolutionSource, \"resolve\"> &\n Partial<Pick<ComponentResolutionSource, \"hasSettled\">>;\n reportError: ContentIrErrorReporter;\n platform: string;\n}\n\n/** Narrow a host (or anything host-shaped) to what the pure route needs. */\nexport function routeEnvOf(host: ContentIrHost): KindRouteEnv {\n return {\n kinds: host.kinds,\n components: host.components,\n reportError: host.reportError,\n platform: host.platform,\n };\n}\n","\"use client\";\n\n/**\n * The one place a host is wired in. Everything React in this package reads its\n * capability from here — no module singleton, no import into an app.\n *\n * A host mounts this ONCE, high enough that every kind-rendering surface is\n * inside it:\n *\n * ```tsx\n * <ContentIrRenderProvider host={matrxContentIrHost}>\n * <App />\n * </ContentIrRenderProvider>\n * ```\n *\n * Reading it outside a provider THROWS rather than falling back to a default.\n * A silent default would render the wrong component for every kind in the app\n * and look like a data problem for weeks.\n */\n\nimport { createContext, useContext, useMemo, type ReactNode } from \"react\";\nimport type { ContentIrHost, KindRouteEnv } from \"./host-types\";\nimport { routeEnvOf } from \"./host-types\";\nimport { DirectiveHostProvider } from \"../directives/directive-host\";\n\nconst ContentIrHostContext = createContext<ContentIrHost | null>(null);\n\nexport interface ContentIrRenderProviderProps {\n host: ContentIrHost;\n children: ReactNode;\n}\n\nexport function ContentIrRenderProvider({\n host,\n children,\n}: ContentIrRenderProviderProps) {\n return (\n <ContentIrHostContext.Provider value={host}>\n <DirectiveHostProvider host={host.directives ?? {}}>{children}</DirectiveHostProvider>\n </ContentIrHostContext.Provider>\n );\n}\n\n/**\n * The wired host, or null. For the rare consumer that can legitimately run\n * without a provider because it was handed its sources explicitly (see\n * `useContentIrKindVersion`). Everything else uses {@link useContentIrHost}.\n */\nexport function useContentIrHostOrNull(): ContentIrHost | null {\n return useContext(ContentIrHostContext);\n}\n\n/** The wired host. Throws outside a provider — see the module doc. */\nexport function useContentIrHost(): ContentIrHost {\n const host = useContentIrHostOrNull();\n if (!host) {\n throw new Error(\n \"[content-ir-react] No ContentIrRenderProvider above this component. \" +\n \"Mount one at the app root with your host adapter (kind definitions, \" +\n \"component resolver, renderBlock, renderValue, reportError).\",\n );\n }\n return host;\n}\n\n/** The wired host narrowed to what the pure route functions take. */\nexport function useKindRouteEnv(): KindRouteEnv {\n const host = useContentIrHost();\n return useMemo(() => routeEnvOf(host), [host]);\n}\n","\"use client\";\n\n/**\n * THE DIRECTIVE HOST CONTRACT — what the directive tier refuses to decide.\n *\n * The package owns the DECISIONS (decode → renderer → card → floor, the\n * position law, never-null rendering) and the card itself. Four things stay\n * host property and enter through named seams:\n *\n * 1. APPLYING a side-effect directive — `POST /directives/confirm` as the\n * user, with the host's auth and base URL. Absent → the Apply control is\n * ABSENT (honest), never a dead button.\n * 2. OPENING an item to read it before approving — a window panel, a dialog,\n * a route. Absent → items are not clickable.\n * 3. COPY controls — host chrome (matrx-frontend's CopyButtons). Absent → none.\n * 4. The NOUN catalog + item-kind lookup — server-derived tables the host\n * mirrors (or reads from its kind catalog's `items` edges).\n * 5. ASKING (`ask`), NAMING a record (`renderRecord`), and READING the ledger\n * (`applyState`) — the consequence before the click, the doors, and the\n * state that survives a reload.\n *\n * A host that has adopted `ContentIrRenderProvider` passes `directives` on its\n * ContentIrHost; a host that has NOT (Workflow Studio, 2026-09-08) mounts\n * `DirectiveHostProvider` alone — the directive tier never requires the full\n * render host, because a chat bubble must be able to draw a directive card\n * identically everywhere, today.\n */\n\nimport { createContext, useContext, type ReactNode } from \"react\";\nimport type {\n DecodedDirective,\n DirectiveItemKindLookup,\n DirectiveNounCatalog,\n} from \"@ai-matrx/content-ir\";\n\nexport interface DirectiveApplyResult {\n applied: number;\n failed: number;\n /**\n * The server's own receipt sentence (\"Deleted task “Ship it”.\"), shown in\n * place of a bare count. Never composed client-side.\n */\n message?: string | null;\n /**\n * The records the apply created or changed, so the tally is a way IN to them\n * (no dead ends). Rendered through `renderRecord`; absent → the sentence alone.\n */\n records?: DirectiveRecordRef[];\n /**\n * For an UPDATE: what each item's fields held just BEFORE the write, by item\n * index (`null` for an item the server did not record). The card draws the\n * applied change from this record — never from a live re-read, which after the\n * apply answers the NEW value (G6A review, 2026-10-02: \"(no change)\" for a\n * change that happened).\n */\n before?: ReadonlyArray<Record<string, unknown> | null>;\n}\n\n/**\n * What the server's action ledger says about ONE block, read on mount so a\n * reloaded page never offers \"Apply\" for work that already ran (reviewer\n * defect 5, 2026-10-02).\n *\n * - `not_applied` — still to do (or partly done: the batch re-applies, and the\n * server replays the members it already holds). The card offers Apply.\n * - `in_flight` — another request is applying it right now. The card says it\n * is finishing and asks again shortly; it offers neither Apply nor a tally.\n * - `applied` — the ledger holds every item. The card shows the tally exactly\n * as a fresh apply would: the ledger's own sentence and the records, as doors.\n */\nexport interface DirectiveApplyState {\n state: \"not_applied\" | \"in_flight\" | \"applied\";\n /** The ledger row's sentence (\"Created task.\"), never composed client-side. */\n message?: string | null;\n /**\n * The records the LATEST apply wrote — the tally's doors. After \"Run again\"\n * that is the newest copy, never the first.\n */\n records?: DirectiveRecordRef[];\n /**\n * How many times the ledger holds this block as applied: 1 for the original,\n * one more for every \"Run again\". Absent → 1. A create that ran twice made\n * two records, and the card says so (reviewer, 2026-10-02).\n */\n copies?: number;\n /**\n * For an applied UPDATE: each item's replaced values, by item index, from the\n * ledger row (`null` where the ledger has none — a row from before 2026-10-02).\n * Absent → the card says only what each field was set to.\n */\n before?: ReadonlyArray<Record<string, unknown> | null>;\n}\n\n/** One record a directive names: the noun it is, and its id. */\nexport interface DirectiveRecordRef {\n noun: string;\n id: string;\n}\n\nexport interface DirectiveRecordProps extends DirectiveRecordRef {\n /**\n * What to show until (or unless) the host resolves the live name —\n * \"Task 1a2b3c4d\". Never blank, never \"Item\".\n */\n fallback: string;\n /** Where it is drawn: an item row, the confirm dialog, or the applied tally. */\n context: \"row\" | \"dialog\" | \"applied\";\n /**\n * True when the record is (about to be) in the trash — the host may still\n * name it but should not promise it opens as a live record.\n */\n trashed?: boolean;\n}\n\n/** What the person is being asked to approve — everything the card knows. */\nexport interface DirectiveAskRequest {\n directive: DecodedDirective;\n /** The items, exactly as they will be sent. */\n items: Record<string, unknown>[];\n /** The noun's human label (\"Task\"), from the host's catalog when it has one. */\n nounLabel: string;\n /**\n * True for \"Run again\": the ledger already holds this block, and a yes runs\n * it a SECOND time (`force`) — another copy for a create, the same fields\n * written again for an update, another delete for a restored record. The\n * question must say so.\n */\n again?: boolean;\n}\n\n/**\n * The shell a card hands its host — the two keys, plus WHERE it sits.\n *\n * `conversationId` is the conversation the card renders in (a chat message),\n * absent in a person's own content (a note). It is the idempotency NAMESPACE:\n * a card in an agent's message must apply under the SAME key as that\n * proposal's Approve button, or the one action could run twice.\n */\nexport interface DirectiveShell {\n __kind: string;\n items: Record<string, unknown>[];\n conversationId?: string;\n /** \"Run again\": skip the ledger's dedup and apply a second time. */\n force?: boolean;\n}\n\nexport interface DirectiveOpenItemOptions {\n /** The item, `__kind`-stamped when the shape has an item kind. */\n item: Record<string, unknown>;\n /** The content-IR kind this item IS, or null when the shape has none. */\n itemKind: string | null;\n /** What the item is called — the same title the card shows. */\n title: string;\n /** \"Run Create agent definition · item 1 of 1\". */\n subtitle: string | null;\n directive: DecodedDirective;\n index: number;\n total: number;\n}\n\nexport interface DirectiveCopyProps {\n label: string;\n /** The value to copy — the whole shell or one item. */\n value: unknown;\n /** The item's kind, when copying an item. */\n kind?: string | null;\n size: \"icon\" | \"xs\";\n}\n\nexport interface DirectiveHost {\n /**\n * Apply a side-effect directive as the user (the server re-validates every\n * item against the registered model and runs the ONE handler; idempotent).\n * Reject with an Error whose message is safe to show.\n */\n confirm?: (shell: DirectiveShell) => Promise<DirectiveApplyResult>;\n /**\n * Ask the person BEFORE anything runs, naming the consequence (the records,\n * what changes). Resolve `true` to go ahead, `false` to leave the card as it\n * was. The Apply control stays idle while this is open — nothing reads as\n * \"applying\" until the person has said yes. Absent → `confirm` is called\n * directly (a host that asks inside `confirm` keeps working, but its card\n * will read \"Applying…\" while its dialog is up).\n */\n ask?: (request: DirectiveAskRequest) => Promise<boolean>;\n /**\n * Has this exact block already been applied? Read ONCE when the card mounts\n * (and again while the answer is `in_flight`), so the card survives a reload:\n * the server ledger, not component state, is what knows. The identity is the\n * server's (it hashes the validated items in the same namespace `confirm`\n * applies in), so a host asks — it never computes. Batch on the host side if\n * a page carries many cards. Reject with an Error when the ledger cannot be\n * read: the card reports it and offers Apply, which is safe because `confirm`\n * is idempotent. Absent → the card starts at Apply, as before.\n */\n applyState?: (shell: DirectiveShell) => Promise<DirectiveApplyState>;\n /**\n * Read the CURRENT values of ONE existing record, so an update says what it\n * replaces — \"Status open → done\", never just \"→ done\" (reviewer,\n * 2026-10-02). Resolve `null` when the record cannot be read (no access, no\n * such record, a noun the host cannot read); reject only for a real failure.\n * Absent → an update row shows the new values alone, as before.\n */\n readRecord?: (ref: DirectiveRecordRef) => Promise<Record<string, unknown> | null>;\n /**\n * Draw ONE existing record by its live name, as a way to open it. Used on\n * update/delete rows (the record that will change), in the host's own ask\n * dialog if it likes, and on the applied tally (what was written). Absent →\n * the fallback text, unlinked.\n */\n renderRecord?: (props: DirectiveRecordProps) => ReactNode;\n /** Open ONE item to read it properly before approving the write. */\n openItem?: (options: DirectiveOpenItemOptions) => void;\n /** Host copy controls. */\n renderCopy?: (props: DirectiveCopyProps) => ReactNode;\n /** The mirrored noun catalog (label / family / title column). */\n nouns?: DirectiveNounCatalog;\n /** slug → the kind ONE item is (THE DIRECTIVE⇄KIND SEAM), or null. */\n itemKind?: DirectiveItemKindLookup;\n /** Where a decode failure or an unhonourable shell is reported. Default: console.error. */\n reportError?: (message: string) => void;\n /**\n * Plain words for a failed apply the host recognises (\"This button can't\n * create a task yet.\" + \"Create it from Tasks instead.\"). The card never shows\n * the server's raw text as its sentence: it shows these words — or its own\n * verb-aware default — and keeps the raw text behind \"Details\". Return null\n * to use the default.\n */\n explainFailure?: (failure: DirectiveFailure) => DirectiveFailureWords | null;\n}\n\n/** One failed apply, as the card knows it. */\nexport interface DirectiveFailure {\n /** The server's / host's own text — technical; shown only behind Details. */\n raw: string;\n directiveClass: string;\n noun: string;\n /** The noun's display label (\"Task\"). */\n nounLabel: string;\n /** Items that failed, when some applied and some did not. */\n failed?: number;\n /** Items in the block. */\n total?: number;\n}\n\n/** What failed and what to do — each one short sentence. */\nexport interface DirectiveFailureWords {\n what: string;\n next: string;\n}\n\nconst DirectiveHostContext = createContext<DirectiveHost | null>(null);\n\n/** Where a directive renders — today, the conversation it sits in (if any). */\nexport interface DirectiveScope {\n conversationId?: string | null;\n}\n\nconst DirectiveScopeContext = createContext<DirectiveScope>({});\n\n/**\n * Tell every card below WHERE it renders. A host draws a chat message's\n * directives inside one of these (or passes `conversationId` to\n * `DirectiveRender`), so the card applies in that conversation's namespace —\n * the same key the agent's proposal approves under. A note mounts none.\n */\nexport function DirectiveScopeProvider({\n conversationId,\n children,\n}: DirectiveScope & { children: ReactNode }) {\n return (\n <DirectiveScopeContext.Provider value={{ conversationId: conversationId ?? null }}>\n {children}\n </DirectiveScopeContext.Provider>\n );\n}\n\nexport function useDirectiveScope(): DirectiveScope {\n return useContext(DirectiveScopeContext);\n}\n\nexport interface DirectiveHostProviderProps {\n host: DirectiveHost;\n children: ReactNode;\n}\n\nexport function DirectiveHostProvider({ host, children }: DirectiveHostProviderProps) {\n return <DirectiveHostContext.Provider value={host}>{children}</DirectiveHostContext.Provider>;\n}\n\nconst EMPTY_HOST: DirectiveHost = {};\n\n/**\n * The wired directive host. Unlike the render host this does NOT throw when\n * unwired: a directive still renders (named, itemised, copy-less, apply-less)\n * — degrading the chrome is honest, hiding the content is not.\n */\nexport function useDirectiveHost(): DirectiveHost {\n return useContext(DirectiveHostContext) ?? EMPTY_HOST;\n}\n"],"mappings":";;;AA2BO,IAAM,uBAA+C,CAAC,WAAW;AACtE,UAAQ,MAAM,gBAAgB,OAAO,OAAO,IAAI,OAAO,OAAO,EAAE;AAClE;;;AC0HO,SAAS,WAAW,MAAmC;AAC5D,SAAO;AAAA,IACL,OAAO,KAAK;AAAA,IACZ,YAAY,KAAK;AAAA,IACjB,aAAa,KAAK;AAAA,IAClB,UAAU,KAAK;AAAA,EACjB;AACF;;;AC1IA,SAAS,iBAAAA,gBAAe,cAAAC,aAAY,eAA+B;;;ACQnE,SAAS,eAAe,kBAAkC;AAmPtD;AApBJ,IAAM,uBAAuB,cAAoC,IAAI;AAOrE,IAAM,wBAAwB,cAA8B,CAAC,CAAC;AA4BvD,SAAS,sBAAsB,EAAE,MAAM,SAAS,GAA+B;AACpF,SAAO,oBAAC,qBAAqB,UAArB,EAA8B,OAAO,MAAO,UAAS;AAC/D;;;AD1PM,gBAAAC,YAAA;AAbN,IAAM,uBAAuBC,eAAoC,IAAI;AAO9D,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,GAAiC;AAC/B,SACE,gBAAAD,KAAC,qBAAqB,UAArB,EAA8B,OAAO,MACpC,0BAAAA,KAAC,yBAAsB,MAAM,KAAK,cAAc,CAAC,GAAI,UAAS,GAChE;AAEJ;AAOO,SAAS,yBAA+C;AAC7D,SAAOE,YAAW,oBAAoB;AACxC;AAGO,SAAS,mBAAkC;AAChD,QAAM,OAAO,uBAAuB;AACpC,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,kBAAgC;AAC9C,QAAM,OAAO,iBAAiB;AAC9B,SAAO,QAAQ,MAAM,WAAW,IAAI,GAAG,CAAC,IAAI,CAAC;AAC/C;","names":["createContext","useContext","jsx","createContext","useContext"]}
package/dist/index.cjs CHANGED
@@ -76,6 +76,7 @@ __export(index_exports, {
76
76
  markKindPartialUnsafe: () => markKindPartialUnsafe,
77
77
  provisionalDirectiveSlug: () => provisionalDirectiveSlug,
78
78
  readIrRouteMarker: () => readIrRouteMarker,
79
+ readsApplyLedger: () => readsApplyLedger,
79
80
  recordFallbackName: () => recordFallbackName,
80
81
  registerDirectiveRenderer: () => registerDirectiveRenderer,
81
82
  resetDirectiveRenderers: () => resetDirectiveRenderers,
@@ -1030,19 +1031,52 @@ function classIcon(directiveClass, noun) {
1030
1031
 
1031
1032
  // directives/ApplyDirectiveButton.tsx
1032
1033
  var import_jsx_runtime4 = require("react/jsx-runtime");
1034
+ var applyOutcomes = /* @__PURE__ */ new WeakMap();
1035
+ var EMPTY_AUTHORITY = {};
1036
+ var outcomeListeners = /* @__PURE__ */ new Set();
1037
+ function outcomesOf(host) {
1038
+ let held = applyOutcomes.get(host);
1039
+ if (!held) {
1040
+ held = /* @__PURE__ */ new Map();
1041
+ applyOutcomes.set(host, held);
1042
+ }
1043
+ return held;
1044
+ }
1045
+ function publishOutcome(host, identity, outcome) {
1046
+ outcomesOf(host).set(identity, outcome);
1047
+ for (const listener of outcomeListeners) listener();
1048
+ }
1049
+ function subscribeOutcomes(listener) {
1050
+ outcomeListeners.add(listener);
1051
+ return () => {
1052
+ outcomeListeners.delete(listener);
1053
+ };
1054
+ }
1055
+ function useApplyOutcome(host, identity) {
1056
+ return (0, import_react4.useSyncExternalStore)(
1057
+ subscribeOutcomes,
1058
+ () => applyOutcomes.get(host)?.get(identity) ?? null,
1059
+ () => null
1060
+ );
1061
+ }
1033
1062
  var FINISH_POLL_MS = 2e3;
1034
1063
  var FINISH_POLLS_MAX = 30;
1064
+ function readsApplyLedger(host, directive) {
1065
+ return !!host.applyState && !!host.confirm && directive.parsed.executes && directive.items.length > 0;
1066
+ }
1035
1067
  function ApplyDirectiveButton({
1036
1068
  directive,
1037
1069
  label = "Apply",
1038
1070
  itemCount,
1039
- onApplied
1071
+ onApplied,
1072
+ onPhase,
1073
+ nothingToChange = false
1040
1074
  }) {
1041
1075
  const host = useDirectiveHost();
1042
1076
  const scope = useDirectiveScope();
1043
1077
  const conversationId = scope.conversationId || void 0;
1044
1078
  const items = directive.items;
1045
- const readsLedger = !!host.applyState && !!host.confirm && directive.parsed.executes && items.length > 0;
1079
+ const readsLedger = readsApplyLedger(host, directive);
1046
1080
  const initial = readsLedger ? { status: "checking" } : { status: "idle" };
1047
1081
  const [state, setState] = (0, import_react4.useState)(initial);
1048
1082
  const [elapsed, setElapsed] = (0, import_react4.useState)(0);
@@ -1056,6 +1090,8 @@ function ApplyDirectiveButton({
1056
1090
  setAskingAgain(false);
1057
1091
  setElapsed(0);
1058
1092
  }
1093
+ const shared = useApplyOutcome(host.confirm ?? EMPTY_AUTHORITY, identity);
1094
+ const view = state.status === "asking" || !shared ? state : shared;
1059
1095
  const currentIdentity = (0, import_react4.useRef)(identity);
1060
1096
  (0, import_react4.useEffect)(() => {
1061
1097
  currentIdentity.current = identity;
@@ -1083,7 +1119,8 @@ function ApplyDirectiveButton({
1083
1119
  failed: 0,
1084
1120
  message: answer.message?.trim() ? answer.message.trim() : null,
1085
1121
  records: answer.records ?? [],
1086
- copies: Math.max(1, answer.copies ?? 1)
1122
+ copies: Math.max(1, answer.copies ?? 1),
1123
+ before: answer.before ?? null
1087
1124
  } : prev
1088
1125
  );
1089
1126
  } else if (answer.state === "in_flight" && polls < FINISH_POLLS_MAX) {
@@ -1108,24 +1145,34 @@ function ApplyDirectiveButton({
1108
1145
  if (timer) clearTimeout(timer);
1109
1146
  };
1110
1147
  }, [shellKey, read, report]);
1148
+ const appliedClean = view.status === "applied" && view.failed === 0 ? view : null;
1149
+ const appliedBefore = appliedClean ? appliedClean.before : null;
1150
+ const phaseKey = view.status === "checking" || view.status === "finishing" ? "pending" : appliedClean ? `applied:${JSON.stringify(appliedBefore)}` : "open";
1111
1151
  (0, import_react4.useEffect)(() => {
1112
- if (state.status !== "applying") return;
1113
- startedAt.current = Date.now();
1152
+ if (!onPhase) return;
1153
+ if (phaseKey === "pending") onPhase({ phase: "pending" });
1154
+ else if (phaseKey === "open") onPhase({ phase: "open" });
1155
+ else onPhase({ phase: "applied", before: appliedBefore });
1156
+ }, [phaseKey, onPhase, appliedBefore]);
1157
+ const applyingSince = view.status === "applying" ? view.startedAt : null;
1158
+ (0, import_react4.useEffect)(() => {
1159
+ if (applyingSince === null) return;
1160
+ startedAt.current = applyingSince;
1114
1161
  const id = setInterval(
1115
1162
  () => setElapsed(Math.round((Date.now() - (startedAt.current ?? 0)) / 1e3)),
1116
1163
  1e3
1117
1164
  );
1118
1165
  return () => clearInterval(id);
1119
- }, [state.status]);
1166
+ }, [applyingSince]);
1120
1167
  if (!directive.parsed.executes) return null;
1121
1168
  const confirm = host.confirm;
1122
1169
  if (!confirm) return null;
1123
1170
  async function handleApply(again = false) {
1124
1171
  if (!confirm) return;
1125
- const before = state;
1172
+ const prior = view;
1126
1173
  const askedFor = identity;
1127
1174
  const stillHere = () => currentIdentity.current === askedFor;
1128
- const priorCopies = state.status === "applied" ? state.copies : 0;
1175
+ const priorCopies = view.status === "applied" ? view.copies : 0;
1129
1176
  if (host.ask) {
1130
1177
  if (again) setAskingAgain(true);
1131
1178
  else setState({ status: "asking" });
@@ -1149,41 +1196,43 @@ function ApplyDirectiveButton({
1149
1196
  if (!stillHere()) return;
1150
1197
  setAskingAgain(false);
1151
1198
  if (!ok) {
1152
- setState(again ? before : { status: "idle" });
1199
+ setState(again ? prior : { status: "idle" });
1153
1200
  return;
1154
1201
  }
1155
1202
  }
1156
1203
  setElapsed(0);
1157
- setState({ status: "applying" });
1204
+ setState((prev) => prev.status === "asking" ? { status: "idle" } : prev);
1205
+ publishOutcome(confirm, askedFor, { status: "applying", startedAt: Date.now() });
1158
1206
  try {
1159
1207
  const shell = { __kind: directive.slug, items };
1160
1208
  if (conversationId) shell.conversationId = conversationId;
1161
1209
  if (again) shell.force = true;
1162
1210
  const result = await confirm(shell);
1163
- if (!stillHere()) return;
1164
- setState({
1211
+ publishOutcome(confirm, askedFor, {
1165
1212
  status: "applied",
1166
1213
  applied: result.applied,
1167
1214
  failed: result.failed,
1168
1215
  message: result.message?.trim() ? result.message.trim() : null,
1169
1216
  records: result.records ?? [],
1170
1217
  // "Run again" made one more; a first apply (or a replay of one) is one.
1171
- copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies)
1218
+ copies: again ? Math.max(1, priorCopies) + 1 : Math.max(1, priorCopies),
1219
+ // "Run again" wrote the same values over themselves; what the FIRST
1220
+ // apply replaced is still the change this block made.
1221
+ before: again && prior.status === "applied" && prior.before ? prior.before : result.before ?? null
1172
1222
  });
1173
- onApplied?.();
1223
+ if (stillHere()) onApplied?.();
1174
1224
  } catch (error) {
1175
- if (!stillHere()) return;
1176
- setState({
1225
+ publishOutcome(confirm, askedFor, {
1177
1226
  status: "error",
1178
1227
  message: error instanceof Error && error.message ? error.message : "That couldn't be applied just now. Please try again."
1179
1228
  });
1180
1229
  }
1181
1230
  }
1182
- if (state.status === "applied") {
1183
- const bad = state.failed > 0;
1231
+ if (view.status === "applied") {
1232
+ const bad = view.failed > 0;
1184
1233
  const trashed = directive.directiveClass === "delete";
1185
1234
  const label2 = nounText;
1186
- const records = host.renderRecord ? state.records : [];
1235
+ const records = host.renderRecord ? view.records : [];
1187
1236
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1188
1237
  "span",
1189
1238
  {
@@ -1197,26 +1246,26 @@ function ApplyDirectiveButton({
1197
1246
  {
1198
1247
  words: failureWords(
1199
1248
  {
1200
- raw: state.message ?? `${state.failed} failed`,
1249
+ raw: view.message ?? `${view.failed} failed`,
1201
1250
  directiveClass: directive.directiveClass,
1202
1251
  noun: directive.noun,
1203
1252
  nounLabel: nounText,
1204
- failed: state.failed,
1205
- total: state.applied + state.failed
1253
+ failed: view.failed,
1254
+ total: view.applied + view.failed
1206
1255
  },
1207
1256
  host.explainFailure
1208
1257
  ),
1209
- raw: state.message
1258
+ raw: view.message
1210
1259
  }
1211
- ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "min-w-0", children: state.message ?? `Applied ${state.applied}` })
1260
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "min-w-0", children: view.message ?? `Applied ${view.applied}` })
1212
1261
  ] }),
1213
- state.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1262
+ view.copies > 1 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1214
1263
  "span",
1215
1264
  {
1216
1265
  className: "shrink-0 text-muted-foreground",
1217
- "data-apply-copies": state.copies,
1266
+ "data-apply-copies": view.copies,
1218
1267
  title: records.length > 0 ? directive.directiveClass === "create" ? "Links to the latest copy." : "Links to the latest run." : void 0,
1219
- children: directive.directiveClass === "create" ? `${state.copies} copies` : `Ran ${state.copies} times`
1268
+ children: directive.directiveClass === "create" ? `${view.copies} copies` : `Ran ${view.copies} times`
1220
1269
  }
1221
1270
  ) : null,
1222
1271
  records.map((record) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "inline-flex min-w-0 items-center", children: host.renderRecord?.({
@@ -1243,7 +1292,7 @@ function ApplyDirectiveButton({
1243
1292
  }
1244
1293
  );
1245
1294
  }
1246
- if (state.status === "checking") {
1295
+ if (view.status === "checking") {
1247
1296
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1248
1297
  "span",
1249
1298
  {
@@ -1255,7 +1304,7 @@ function ApplyDirectiveButton({
1255
1304
  }
1256
1305
  );
1257
1306
  }
1258
- if (state.status === "finishing") {
1307
+ if (view.status === "finishing") {
1259
1308
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1260
1309
  "span",
1261
1310
  {
@@ -1270,22 +1319,25 @@ function ApplyDirectiveButton({
1270
1319
  );
1271
1320
  }
1272
1321
  const destructive = directive.directiveClass === "delete";
1322
+ const muted = nothingToChange && view.status === "idle";
1273
1323
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "inline-flex items-center gap-2", children: [
1324
+ muted ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-xs text-muted-foreground", "data-apply-nothing": "", children: "Nothing to change" }) : null,
1274
1325
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1275
1326
  "button",
1276
1327
  {
1277
1328
  type: "button",
1278
1329
  onClick: () => void handleApply(),
1279
- disabled: state.status === "applying" || state.status === "asking" || items.length === 0,
1280
- "data-apply-tone": destructive ? "destructive" : "default",
1281
- className: `inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs font-medium disabled:opacity-50 ${destructive ? "border-destructive/50 bg-background text-destructive hover:bg-destructive/10" : "border-border bg-background hover:bg-accent"}`,
1330
+ disabled: view.status === "applying" || view.status === "asking" || items.length === 0,
1331
+ "data-apply-tone": muted ? "muted" : destructive ? "destructive" : "default",
1332
+ title: muted ? "It sets no fields, so applying it changes nothing." : void 0,
1333
+ className: `inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs font-medium disabled:opacity-50 ${muted ? "border-border/60 bg-background text-muted-foreground hover:bg-accent" : destructive ? "border-destructive/50 bg-background text-destructive hover:bg-destructive/10" : "border-border bg-background hover:bg-accent"}`,
1282
1334
  children: [
1283
- state.status === "applying" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PlayIcon, { className: "h-3.5 w-3.5" }),
1284
- state.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
1335
+ view.status === "applying" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PlayIcon, { className: "h-3.5 w-3.5" }),
1336
+ view.status === "applying" ? `Applying${itemCount ? ` ${itemCount} ${itemCount === 1 ? "item" : "items"}` : ""}\u2026${elapsed ? ` ${elapsed}s` : ""}` : label
1285
1337
  ]
1286
1338
  }
1287
1339
  ),
1288
- state.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1340
+ view.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1289
1341
  "span",
1290
1342
  {
1291
1343
  className: "inline-flex max-w-sm items-start gap-1 text-xs text-destructive",
@@ -1297,14 +1349,14 @@ function ApplyDirectiveButton({
1297
1349
  {
1298
1350
  words: failureWords(
1299
1351
  {
1300
- raw: state.message,
1352
+ raw: view.message,
1301
1353
  directiveClass: directive.directiveClass,
1302
1354
  noun: directive.noun,
1303
1355
  nounLabel: nounText
1304
1356
  },
1305
1357
  host.explainFailure
1306
1358
  ),
1307
- raw: state.message
1359
+ raw: view.message
1308
1360
  }
1309
1361
  )
1310
1362
  ]
@@ -1474,12 +1526,13 @@ function ItemRow({
1474
1526
  directive,
1475
1527
  item,
1476
1528
  index,
1477
- total
1529
+ total,
1530
+ phase
1478
1531
  }) {
1479
1532
  const host = useDirectiveHost();
1480
1533
  const recordId = TARGETING_CLASSES.has(directive.directiveClass) ? itemRecordId(item) : null;
1481
1534
  if (recordId) {
1482
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TargetRow, { directive, item, recordId });
1535
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TargetRow, { directive, item, recordId, index, phase });
1483
1536
  }
1484
1537
  const titleColumn = (0, import_content_ir7.nounTitleColumn)(directive.noun, host.nouns);
1485
1538
  const title = (0, import_content_ir7.itemTitle)(item, titleColumn, index, total);
@@ -1566,16 +1619,20 @@ function DirectiveChangeList({
1566
1619
  function TargetRow({
1567
1620
  directive,
1568
1621
  item,
1569
- recordId
1622
+ recordId,
1623
+ index,
1624
+ phase
1570
1625
  }) {
1571
1626
  const host = useDirectiveHost();
1572
1627
  const fallback = recordFallbackName((0, import_content_ir7.nounLabel)(directive.noun, host.nouns), recordId);
1573
1628
  const isUpdate = directive.directiveClass === "update";
1574
- const current = useRecordValues(
1575
- isUpdate ? host.readRecord : void 0,
1576
- isUpdate ? { noun: directive.noun, id: recordId } : null
1629
+ const readsLive = isUpdate && phase.phase === "open";
1630
+ const live = useRecordValues(
1631
+ readsLive ? host.readRecord : void 0,
1632
+ readsLive ? { noun: directive.noun, id: recordId } : null
1577
1633
  );
1578
- const changes = isUpdate ? itemChanges(item, current.values, { titleColumn: (0, import_content_ir7.nounTitleColumn)(directive.noun, host.nouns) }) : [];
1634
+ const compareTo = phase.phase === "applied" ? phase.before?.[index] ?? null : readsLive ? live.values : null;
1635
+ const changes = isUpdate ? itemChanges(item, compareTo, { titleColumn: (0, import_content_ir7.nounTitleColumn)(directive.noun, host.nouns) }) : [];
1579
1636
  const name = host.renderRecord ? host.renderRecord({ noun: directive.noun, id: recordId, fallback, context: "row" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate font-medium text-foreground", children: fallback });
1580
1637
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "py-1 text-xs", "data-directive-target": recordId, children: [
1581
1638
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
@@ -1592,6 +1649,10 @@ function SideEffectDirectiveCard({ directive }) {
1592
1649
  const total = items.length;
1593
1650
  const display = (0, import_content_ir7.directiveDisplay)(directive.directiveClass, directive.noun, host.nouns);
1594
1651
  const Icon = classIcon(directive.directiveClass, directive.noun);
1652
+ const [phase, setPhase] = (0, import_react6.useState)(
1653
+ () => readsApplyLedger(host, directive) ? { phase: "pending" } : { phase: "open" }
1654
+ );
1655
+ const nothingToChange = directive.directiveClass === "update" && total > 0 && items.every((item) => itemChanges(item).length === 0);
1595
1656
  const shown = expanded ? items : items.slice(0, ROWS_BEFORE_FOLD);
1596
1657
  const hidden = total - shown.length;
1597
1658
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "my-3 rounded-md border border-border bg-card px-2.5 py-1.5", "data-directive": directive.slug, children: [
@@ -1606,13 +1667,21 @@ function SideEffectDirectiveCard({ directive }) {
1606
1667
  ] }),
1607
1668
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ml-auto flex items-center gap-1", children: [
1608
1669
  host.renderCopy?.({ label: display.title, value: directive.shell, size: "icon" }),
1609
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ApplyDirectiveButton, { directive, itemCount: total })
1670
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1671
+ ApplyDirectiveButton,
1672
+ {
1673
+ directive,
1674
+ itemCount: total,
1675
+ onPhase: setPhase,
1676
+ nothingToChange
1677
+ }
1678
+ )
1610
1679
  ] })
1611
1680
  ] }),
1612
1681
  total === 0 ? (
1613
1682
  // Stated, never silent: an empty batch is a real (and suspicious) state.
1614
1683
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "mt-1 text-xs text-muted-foreground", children: "No items \u2014 nothing would be written." })
1615
- ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ItemRow, { directive, item, index: i, total }, i)) }),
1684
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mt-0.5 divide-y divide-border/60", children: shown.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ItemRow, { directive, item, index: i, total, phase }, i)) }),
1616
1685
  hidden > 0 || expanded ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1617
1686
  "button",
1618
1687
  {