The catalog snapshot

Version: 1

A catalog snapshot is a tenant's catalog as plain JSON: the adapters it can construct and their schemas, its credential names, its plugins, the models it can reach, and its saved movement files. The checker, the editor and both engines read it; nothing in it is a live object, and it never carries a credential's secret or id (those travel beside it, as a run's credentialIds).

Versions #

The shapes #

The shapes are TypeScript-ish: x?: T may be absent, and an absent member and one that is undefined are the same.

interface CatalogSnapshot {
  contractVersion?: number;                       // absent: 1
  adapters: Record<string, AdapterSnapshot>;      // by adapter type
  credentials: Record<string, { adapters: string[]; credential?: string } | { adapter: string }>;   // by import name; `{ adapter }` is the older single-adapter form; `credential`: the module's declared credential it fills
  plugins: Record<string, PluginSpec>;            // by key: module and name (`@lf/web/render`) for a package's plugin, else the name
  files?: Record<string, { source: string }>;     // saved movement files, by import path
  models?: string[];                              // logical model names this deployment reaches; absent: models unchecked
}

interface AdapterSnapshot extends AdapterSpec {
  schemas: Record<string, InstanceSchema>;        // by instance key: the credential import name, plus `::<entry position>` when the construction pins one; '' is credential-free
  schemaNotes?: Record<string, string[]>;         // why a key's schema is missing, keyed like `schemas`
  constructionArgOptions?: Record<string, Record<string, string[]>>;   // entry-position arg values, by credential import name then arg name
}

interface AdapterSpec {
  constructionArgs: ConstructionArg[];
  declaredCredentials?: string[];                 // a module's declared credentials, by name; a credential naming another (`credential`) is refused at check
  canFire?: boolean;                              // absent on a known spec: it cannot fire anything
  triggerConfig?: string[];
  triggerConfigOptions?: Record<string, string[]>;
  defaultEvents?: string[];
  triggerConfigRequired?: string[];
  triggerConfigRequiredWhy?: Record<string, string>;
  triggerConfigFormats?: Record<string, 'cron' | 'timezone' | 'fields' | 'strings'>;
  connectActions?: Array<{ kind: string; label: string }>;
  connect?: 'oauth' | 'key-entry' | 'intrinsic' | 'handshake' | 'app-only';
  triggerExpectation?: string;
  schemaShape?: 'static' | 'introspected';
  description?: string;                           // the adapter's own words for itself (its manifest's `description`); shown, never checked
}

interface ConstructionArg {
  name: string;
  kind: 'credential' | 'position';
  required: boolean;
  optionsFromType?: string;
  label?: string;
}

interface PluginSpec {
  args: string[];
  priced: boolean;
  argTypes?: Record<string, SchemaFieldType>;   // each argument's declared input type, by name; absent for one nobody typed
  requiredArgs?: string[];
  effects?: { reads?: string[]; writes?: string[]; ai?: boolean; now?: boolean; suspend?: boolean };
  fedByExtraction?: boolean;
  fedArgs?: Array<{ name: string; required?: true }>;
  output?: PluginOutput;
  earlierOutputs?: Array<{ before: number; output: PluginOutput }>;   // the output under language versions before `before`
  package?: string;   // the package tag it is a member of (`@lf/web`); absent when it is in none
  name?: string;      // the name a program spells it (`render`), when the key is not that; absent: the key
  description?: string;   // what it does, in its author's words (its signature's `description`); shown, never checked
}

A plugin's arguments by position (language version 5). A call passes the plugin's required inputs by position, then its other inputs in one settings record (fetchUrl(u), research(n, { context: s })). The positions are the spec's required fedArgs, then its requiredArgs in args order; a through [ … ] stage, which is never fed by its author, takes only the requiredArgs. The spec keeps a plugin's fed inputs apart from its author's, so it does not say how the two interleave in the manifest; every plugin declares its fed input first, so fed-then-author is the manifest's declared order. A settings key is an input's name exactly as args (or, on a plain call, fedArgs) spells it. The call is bound to the named config before the plugin is invoked, so the wire is unchanged: the plugin is handed { url: … } as under version 4. One binding does this in each language (Rust PluginSpec::bind_args, TypeScript bindPluginArgs); both checkers and both engines use it.

A plugin's output that is one file. A value output of type file is the file the host put at the answer's data.value, not the fetched text (a PDF: pdf.render(text)).

A plugin package (language version 5). A spec's package is a tag (@lf/web), compared exactly and never parsed. An import from plugins whose name equals some spec's package imports that package: import { `@lf/web` as web } from plugins. Its members are the specs that declare that tag, each called by its key in camelCase after the package's local name (fetch_url → web.fetchUrl(u), through [web.linkedinEnrichment]). A member call is an ordinary plugin call: it binds by position as above, and the plugin is invoked by its catalog key with the named config. The platform's catalog lists two: @lf/web and @lf/pdf (pdf.render(text)). A package's plugin is keyed by module and name (@lf/pdf/render, @lf/web/render), so two modules can each serve a render; the spec's name is what a program writes. A bare import (import { fetch_url } from plugins, as versions 1 to 4 write it) finds the one plugin of that name, and is refused, naming the packages, when several have it. The key is compared, never parsed. name is optional and additive, so the version stays 1. The member is optional and additive, both readers carry it, so the version stays 1.

type PluginOutput =
  | { kind: 'value'; type: SchemaFieldType }
  | { kind: 'record'; fields: Record<string, SchemaFieldType> }
  | { kind: 'records'; fields: Record<string, SchemaFieldType> };

type SchemaFieldType =
  | 'text' | 'number' | 'boolean' | 'date' | 'datetime' | 'file' | 'json' | 'absent'
  | { kind: 'list'; of: SchemaFieldType; unordered?: true }
  | { kind: 'tuple'; of: Array<SchemaFieldType | null> }
  | { kind: 'dict'; of: SchemaFieldType }
  | { kind: 'enum'; options: string[]; open?: { allowPattern?: string } }
  | { kind: 'maybeAbsent'; of: SchemaFieldType };

interface InstanceSchema {
  positions: Record<string, PositionSchema>;
  collections: Record<string, { target: string; capability?: EdgeCapability }>;
  unions?: Record<string, string[]>;
  unionDisplayNames?: Record<string, string>;
  writableRoots: Record<string, WritableRootSchema>;
  createShapes?: Record<string, WritableRootSchema>;
  supportsInPlaceUpdate?: boolean;
  edgesCarryProperties?: boolean;
  refinements?: Record<string, string>;
  selectedMembers?: Record<string, string>;
  narrowBy?: Record<string, string[]>;
  genericLandings?: Record<string, string>;
  eventPosition?: string;
  eventPositions?: Array<{ position: string; on?: string[] }>;
  eventNarrowingKeys?: string[];
  eventNarrowingValues?: Record<string, Record<string, string[]>>;
}

interface PositionSchema {
  properties: Record<string, SchemaFieldType>;
  edges: Record<string, EdgeSchema>;
  displayName?: string;
  propertyCapabilities?: Record<string, FieldCapability>;
  openProperties?: boolean;
  undescribed?: boolean;
  pending?: boolean;
  writeOnlyProperties?: string[];
  ambiguousProperties?: string[];
  deletable?: boolean;                            // the system removes a record of this type; under language version 5 a `delete` of a type without it is refused
  description?: string;                           // the adapter's words for the type (its describe's, else its entry point's, `description`); shown, never checked
  fieldDocs?: Record<string, string>;             // the adapter's words for each field it described, readable or write-only, by name
}

interface WritableRootSchema {
  fields: Record<string, SchemaFieldType>;
  resultShape: Record<string, SchemaFieldType>;
  discriminated?: { discriminant: string; variants: Record<string, WritableRootSchema> };
  writeUnion?: { variants: Array<{ name: string; fields: string[] }> };
  position?: string;
  nativeUniqueness?: string[][];
  requiredEdges?: Array<{ edge: string; from: string }>;
  edges?: Record<string, EdgeSchema>;
  requiredFields?: string[];
  fieldDocs?: Record<string, string>;             // the writable fields' words only; the position's `fieldDocs` holds every field's
  fuzzyResolution?: true | string[];
  uniquenessAuthorable?: boolean;
}

interface EdgeSchema {
  target: string;
  polymorphic?: boolean;
  required?: boolean;
  capability?: EdgeCapability;
  sequenced?: 'chronological' | 'document' | 'arrival';
  ephemeral?: boolean;
  readable?: boolean;
  writable?: boolean;
  linkable?: boolean;
  requiresLiveRecord?: boolean;
  subject?: boolean;
  awaitable?: boolean;
  resolvesEmpty?: boolean;
  watchable?: boolean;
  genericOver?: { field: string; onNonLiteral?: 'error' | 'warn' };
  resources?: true;                               // the system's resources edge (the describe's reserved `_resources` reference): walked by its name, sent to the adapter by the reserved field id
  description?: string;                           // the adapter's words for the edge (its reference's `description`); shown, never checked
}

interface EdgeCapability {
  filter?: 'bounded' | 'native';
  order?: 'bounded' | 'native';
  supportsLimit: boolean;
  naturalOrder?: NaturalOrder;
}

interface NaturalOrder {
  field: string;
  direction: 'asc' | 'desc';
}

interface FieldCapability {
  filterOperators?: Array<'eq' | 'neq' | 'contains' | 'gt' | 'gte' | 'lt' | 'lte' | 'exists' | 'in' | 'within'>;
  orderable?: boolean;
}

EdgeSchema is the catalog's (packages/movement-lang/catalog/types.ts); EdgeCapability, NaturalOrder and FieldCapability are shared with the expression layer (packages/movement-lang/expression/types.ts). A test holds the member names and optional marks of every interface on this page to those sources. A schema's field types are SchemaFieldType only: the Rust reader refuses a field typed with what only the checker's typing builds (a record, a union).

The file type is the envelope that makes a value a file. Both engines read a position's property types (PositionSchema.properties, the read wrapper's natural type) and a plugin output's field types (PluginOutput), and make a file of each value at a file position — file itself, a list's or a dict's members of that type, a tuple's slots, a maybe-absent one — where the value spells a file's descriptor (engine-contracts fileDescriptorOf). A value at any other type is data, whatever keys it carries; a value at a file type that spells no descriptor stays data too. No reader classifies a value by its keys.

Changes #