// ---------------------------------------------------------------------------// Artifacts — saved generative-UI components. Row → public projection plus the// operation inputs; the executor stitches these into `executor.artifacts` and// the core `artifacts` HTTP group.//// An artifact IS the stored JSX source plus the title/description an agent// matches against ("show me my active users dashboard"). Owner-scoped like a// connection: created at the `user` tier, readable through the same owner// policy, so org-tier sharing later needs no new machinery.// --------------------------------------------------------------------------- import { Option, Schema } from "effect"; import type { ArtifactRow, ArtifactSummaryRow } from "./core-schema";import { ArtifactId, ConnectionName, IntegrationSlug, Owner } from "./ids"; /** * Which connection one integration role in the artifact's code resolves to. * * Artifact source names an integration and, when it needs two accounts of one, * a role — `tools.linear("prod").issues.list`. The three address segments that * were taken OUT of the source live here, so the host can put them back at * invoke time. Storing the tier and connection NAME (rather than a connection * row id) keeps this a pure address projection: it re-resolves against whoever * is viewing, which is what makes rebinding a shared artifact possible later. */export const ArtifactBinding = Schema.Struct({ integration: IntegrationSlug, owner: Owner, connection: ConnectionName,}).annotate({ description: "The connection one integration role in an artifact's code resolves to, as the three address segments the source omits.",});export type ArtifactBinding = typeof ArtifactBinding.Type; /** `role -> connection` for every integration role the artifact's code uses. */export const ArtifactBindings = Schema.Record(Schema.String, ArtifactBinding).annotate({ description: "Every integration role an artifact's code uses, mapped to the connection it runs as.",});export type ArtifactBindings = typeof ArtifactBindings.Type; const decodeBindings = Schema.decodeUnknownOption(ArtifactBindings);const decodeJsonString = Schema.decodeUnknownOption(Schema.UnknownFromJsonString); /** * The stored column, as bindings. * * JSON arrives as an object on Postgres and as a string on SQLite, and is * absent entirely on rows written before bindings existed. Absent is `null`, * which is meaningful: it is what marks an artifact as pre-contract and lets * its old five-segment paths run as written. * * A column that is present but undecodable is therefore NOT null — it becomes * an empty binding set, so every role in the artifact fails closed with * `binding_unresolved` instead of falling through the pre-contract door. Only * this host writes the column, so the case means corruption; failing visibly * on the artifact is the honest outcome. */const bindingsFromColumn = (value: unknown): ArtifactBindings | null => { if (value === null || value === undefined) return null; const json = typeof value === "string" ? decodeJsonString(value) : Option.some(value); return Option.flatMap(json, decodeBindings).pipe(Option.getOrElse(() => ({})));}; /** * What the gallery draws for an artifact, when it has anything to draw. * * Always sanitized markup, produced at one of two moments: * * - at CREATE, from the smoke render — the artifact's real composition in its * LOADING state (frames, tables, skeletons), containing no data at all * because nothing was fetched; * - at OPEN, from the settled render in a viewer's browser — the same * composition with real numbers, rows and labels in it. * * Pixels were the intent for the second one and are not achievable: the render * frame is sandboxed into an opaque origin, so every image it can build taints * a canvas and `toDataURL` throws. See `captureSettledHtml` in the shell's * inner renderer for the measurement. Markup keeps what the upgrade was FOR — * a card that shows the artifact's real content — and gives up only the * typeface and pixel-exact fidelity. * * The two are one type because the console renders them identically. They are * NOT interchangeable for sharing, which is why the source is recorded: see the * data-safety note on `Artifact.preview`. */export type ArtifactPreview = { readonly kind: "layout"; readonly markup: string;}; /** * Read the stored column as a preview. * * Absent, empty, or anything that is not a string is `null`, and the caller * falls back to the schematic. Anything else is markup this host sanitized * before storing — nothing else writes the column. */export const previewFromColumn = (value: unknown): ArtifactPreview | null => typeof value === "string" && value !== "" ? { kind: "layout", markup: value } : null; export interface Artifact { readonly id: ArtifactId; readonly owner: Owner; readonly title: string; /** Model-supplied prose used for agent matching. Null when none was given. */ readonly description: string | null; /** The JSX source. Only the full read carries it — lists stay light. */ readonly code: string; /** * Which connection each integration role in `code` runs as. * * `null` means the artifact predates the binding contract: its source still * carries full `tools.<integration>.<tier>.<connection>.<tool>` addresses and * executes them as written. Every artifact saved since is bound, even if the * binding set is empty (code that calls no integration). */ readonly bindings: ArtifactBindings | null; /** * What the gallery draws for this artifact, or `null` to fall back to the * schematic. * * DATA SAFETY. A `layout` preview is derived from `code` alone and is safe to * treat as shared artifact metadata. An `image` preview is NOT: it is pixels * of the artifact rendered with whatever data the VIEWER could see. Today * every artifact is viewer-owned, so the row and the viewer are the same * person and storing it here is correct. When org-tier sharing lands, an * image preview must move to per-viewer storage or be excluded from shared * views — otherwise one member's numbers become another's thumbnail. The * `layout` half stays shareable either way, which is why both kinds exist. */ readonly preview: ArtifactPreview | null; readonly createdAt: Date; readonly updatedAt: Date;} /** What a list returns: everything except the source and the bindings that * interpret it. Both are only meaningful to a renderer, and both are heavy. * The preview stays, because the list is what draws it. */export type ArtifactSummary = Omit<Artifact, "code" | "bindings">; /** Create a new artifact, or overwrite an existing one in place when `id` names * one. v1 has no version history: a save replaces the stored source. */export interface SaveArtifactInput { readonly id?: string; readonly title: string; readonly description?: string | null; readonly code: string; /** * The connections `code`'s integration roles resolve to. Written on every * save, since code and bindings are one fact: replacing the source without * its bindings would leave the artifact interpreting new roles through old * connections. */ readonly bindings?: ArtifactBindings | null; /** * The layout preview for this source, already sanitized. * * Written on every save for the same reason as `bindings`: it is an * interpretation of `code`, so carrying the old one forward under new source * would show the gallery an artifact that no longer exists. Omitted or * `null` clears it — including when a save supersedes an image preview, * which is correct, since that image is of the previous version. */ readonly preview?: string | null;} export interface RenameArtifactInput { readonly id: string; readonly title: string;} export interface RemoveArtifactInput { readonly id: string;} /** * Upgrade an artifact's preview to a raster snapshot of a settled render. * * Separate from `SaveArtifactInput` because it is a different act by a * different party: a save is the MODEL replacing the source, this is a VIEWER * reporting what the artifact actually looked like. It touches no other column * and deliberately does not move `updated_at` — being looked at is not an edit, * and the gallery sorts by that timestamp. */export interface SetArtifactPreviewInput { readonly id: string; /** A `data:image/...` URL. Rejected by the handler if it is anything else. */ readonly preview: string;} const asDate = (value: Date | number | string): Date => value instanceof Date ? value : new Date(value); export const rowToArtifactSummary = (row: ArtifactSummaryRow): ArtifactSummary => ({ id: ArtifactId.make(row.id), owner: row.owner as Owner, title: row.title, description: row.description ?? null, preview: previewFromColumn(row.preview), createdAt: asDate(row.created_at), updatedAt: asDate(row.updated_at),}); export const rowToArtifact = (row: ArtifactRow): Artifact => ({ ...rowToArtifactSummary(row), code: row.code, bindings: bindingsFromColumn(row.bindings),});
// ---------------------------------------------------------------------------
// Artifacts — saved generative-UI components. Row → public projection plus the
// operation inputs; the executor stitches these into `executor.artifacts` and
// the core `artifacts` HTTP group.
//
// An artifact IS the stored JSX source plus the title/description an agent
// matches against ("show me my active users dashboard"). Owner-scoped like a
// connection: created at the `user` tier, readable through the same owner
// policy, so org-tier sharing later needs no new machinery.
// ---------------------------------------------------------------------------
import { Option, Schema } from "effect";
import type { ArtifactRow, ArtifactSummaryRow } from "./core-schema";
import { ArtifactId, ConnectionName, IntegrationSlug, Owner } from "./ids";
/**
* Which connection one integration role in the artifact's code resolves to.
*
* Artifact source names an integration and, when it needs two accounts of one,
* a role — `tools.linear("prod").issues.list`. The three address segments that
* were taken OUT of the source live here, so the host can put them back at
* invoke time. Storing the tier and connection NAME (rather than a connection
* row id) keeps this a pure address projection: it re-resolves against whoever
* is viewing, which is what makes rebinding a shared artifact possible later.
*/
export const ArtifactBinding = Schema.Struct({
integration: IntegrationSlug,
owner: Owner,
connection: ConnectionName,
}).annotate({
description:
"The connection one integration role in an artifact's code resolves to, as the three address segments the source omits.",
});
export type ArtifactBinding = typeof ArtifactBinding.Type;
/** `role -> connection` for every integration role the artifact's code uses. */
export const ArtifactBindings = Schema.Record(Schema.String, ArtifactBinding).annotate({
description:
"Every integration role an artifact's code uses, mapped to the connection it runs as.",
});
export type ArtifactBindings = typeof ArtifactBindings.Type;
const decodeBindings = Schema.decodeUnknownOption(ArtifactBindings);
const decodeJsonString = Schema.decodeUnknownOption(Schema.UnknownFromJsonString);
/**
* The stored column, as bindings.
*
* JSON arrives as an object on Postgres and as a string on SQLite, and is
* absent entirely on rows written before bindings existed. Absent is `null`,
* which is meaningful: it is what marks an artifact as pre-contract and lets
* its old five-segment paths run as written.
*
* A column that is present but undecodable is therefore NOT null — it becomes
* an empty binding set, so every role in the artifact fails closed with
* `binding_unresolved` instead of falling through the pre-contract door. Only
* this host writes the column, so the case means corruption; failing visibly
* on the artifact is the honest outcome.
*/
const bindingsFromColumn = (value: unknown): ArtifactBindings | null => {
if (value === null || value === undefined) return null;
const json = typeof value === "string" ? decodeJsonString(value) : Option.some(value);
return Option.flatMap(json, decodeBindings).pipe(Option.getOrElse(() => ({})));
};
/**
* What the gallery draws for an artifact, when it has anything to draw.
*
* Always sanitized markup, produced at one of two moments:
*
* - at CREATE, from the smoke render — the artifact's real composition in its
* LOADING state (frames, tables, skeletons), containing no data at all
* because nothing was fetched;
* - at OPEN, from the settled render in a viewer's browser — the same
* composition with real numbers, rows and labels in it.
*
* Pixels were the intent for the second one and are not achievable: the render
* frame is sandboxed into an opaque origin, so every image it can build taints
* a canvas and `toDataURL` throws. See `captureSettledHtml` in the shell's
* inner renderer for the measurement. Markup keeps what the upgrade was FOR —
* a card that shows the artifact's real content — and gives up only the
* typeface and pixel-exact fidelity.
*
* The two are one type because the console renders them identically. They are
* NOT interchangeable for sharing, which is why the source is recorded: see the
* data-safety note on `Artifact.preview`.
*/
export type ArtifactPreview = {
readonly kind: "layout";
readonly markup: string;
};
/**
* Read the stored column as a preview.
*
* Absent, empty, or anything that is not a string is `null`, and the caller
* falls back to the schematic. Anything else is markup this host sanitized
* before storing — nothing else writes the column.
*/
export const previewFromColumn = (value: unknown): ArtifactPreview | null =>
typeof value === "string" && value !== "" ? { kind: "layout", markup: value } : null;
export interface Artifact {
readonly id: ArtifactId;
readonly owner: Owner;
readonly title: string;
/** Model-supplied prose used for agent matching. Null when none was given. */
readonly description: string | null;
/** The JSX source. Only the full read carries it — lists stay light. */
readonly code: string;
/**
* Which connection each integration role in `code` runs as.
*
* `null` means the artifact predates the binding contract: its source still
* carries full `tools.<integration>.<tier>.<connection>.<tool>` addresses and
* executes them as written. Every artifact saved since is bound, even if the
* binding set is empty (code that calls no integration).
*/
readonly bindings: ArtifactBindings | null;
/**
* What the gallery draws for this artifact, or `null` to fall back to the
* schematic.
*
* DATA SAFETY. A `layout` preview is derived from `code` alone and is safe to
* treat as shared artifact metadata. An `image` preview is NOT: it is pixels
* of the artifact rendered with whatever data the VIEWER could see. Today
* every artifact is viewer-owned, so the row and the viewer are the same
* person and storing it here is correct. When org-tier sharing lands, an
* image preview must move to per-viewer storage or be excluded from shared
* views — otherwise one member's numbers become another's thumbnail. The
* `layout` half stays shareable either way, which is why both kinds exist.
*/
readonly preview: ArtifactPreview | null;
readonly createdAt: Date;
readonly updatedAt: Date;
}
/** What a list returns: everything except the source and the bindings that
* interpret it. Both are only meaningful to a renderer, and both are heavy.
* The preview stays, because the list is what draws it. */
export type ArtifactSummary = Omit<Artifact, "code" | "bindings">;
/** Create a new artifact, or overwrite an existing one in place when `id` names
* one. v1 has no version history: a save replaces the stored source. */
export interface SaveArtifactInput {
readonly id?: string;
readonly title: string;
readonly description?: string | null;
readonly code: string;
/**
* The connections `code`'s integration roles resolve to. Written on every
* save, since code and bindings are one fact: replacing the source without
* its bindings would leave the artifact interpreting new roles through old
* connections.
*/
readonly bindings?: ArtifactBindings | null;
/**
* The layout preview for this source, already sanitized.
*
* Written on every save for the same reason as `bindings`: it is an
* interpretation of `code`, so carrying the old one forward under new source
* would show the gallery an artifact that no longer exists. Omitted or
* `null` clears it — including when a save supersedes an image preview,
* which is correct, since that image is of the previous version.
*/
readonly preview?: string | null;
}
export interface RenameArtifactInput {
readonly id: string;
readonly title: string;
}
export interface RemoveArtifactInput {
readonly id: string;
}
/**
* Upgrade an artifact's preview to a raster snapshot of a settled render.
*
* Separate from `SaveArtifactInput` because it is a different act by a
* different party: a save is the MODEL replacing the source, this is a VIEWER
* reporting what the artifact actually looked like. It touches no other column
* and deliberately does not move `updated_at` — being looked at is not an edit,
* and the gallery sorts by that timestamp.
*/
export interface SetArtifactPreviewInput {
readonly id: string;
/** A `data:image/...` URL. Rejected by the handler if it is anything else. */
readonly preview: string;
}
const asDate = (value: Date | number | string): Date =>
value instanceof Date ? value : new Date(value);
export const rowToArtifactSummary = (row: ArtifactSummaryRow): ArtifactSummary => ({
id: ArtifactId.make(row.id),
owner: row.owner as Owner,
title: row.title,
description: row.description ?? null,
preview: previewFromColumn(row.preview),
createdAt: asDate(row.created_at),
updatedAt: asDate(row.updated_at),
});
export const rowToArtifact = (row: ArtifactRow): Artifact => ({
...rowToArtifactSummary(row),
code: row.code,
bindings: bindingsFromColumn(row.bindings),
});