211 lines · 9.0 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// 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),});