import { type AgentSettings } from "@lobu/core"; import type { InferenceModality } from "../../../config/define.js"; import type { RemoteAgent, RemoteAuthProfile, RemoteAutomation, RemoteConnection, RemoteConnectorDefinition, RemoteEntityType, RemoteFeed, RemoteInferenceProvider, RemoteRelationshipType, UpdateConnectionPayload, UpdateFeedPayload } from "./client.js"; import type { DesiredAgent, DesiredAuthProfile, DesiredAutomationTrigger, DesiredConnection, DesiredConnectorDefinition, DesiredEntityType, DesiredFeed, DesiredOrgProvider, DesiredRelationshipType, DesiredState, DesiredAutomation } from "./desired-state.js"; import { type AutomationSource } from "./shared.js"; type DiffVerb = "create" | "update" | "noop" | "drift" | "delete"; interface BaseRow { verb: DiffVerb; /** Stable identifier for matching messages and UI. */ id: string; } /** * The desired/remote/changed-fields triple shared by every per-resource row * kind. `changedFields` carries field-level changes when verb === "update" and * remains the sole input to apply-cmd routing and the audit `changed_fields`. */ interface ResourceRow extends BaseRow { desired?: D; remote?: R; changedFields?: string[]; } /** * Before/after pairs are restricted to the baseline-aware memory-schema rows. * The other resource rows include provider keys, BYO connection config, and * auth profiles whose values must never be printed in a plan, so they have no * field to populate and the unsafe version does not typecheck. * * Automations are deliberately excluded too, though they are not secret-bearing: * their values reach the plan only through the two-way `diffAutomation` row, whose * field identifiers differ from the projection's (`agent` vs `managed_agent_id`), and * the name-mapping table that reconciled them was not worth its weight. */ interface ValuePreviewRow { changedValues?: Array<{ field: string; from: unknown; to: unknown; }>; } export interface AgentDiffRow extends ResourceRow { kind: "agent"; } export interface SettingsDiffRow extends BaseRow { kind: "settings"; desired?: Partial; changedFields?: string[]; } export interface EntityTypeDiffRow extends ResourceRow, ValuePreviewRow { kind: "entity-type"; } export interface RelationshipTypeDiffRow extends ResourceRow, ValuePreviewRow { kind: "relationship-type"; } export interface AutomationDiffRow extends ResourceRow { kind: "automation"; /** * Field names that require a `create_version` + `upgrade` (vs a plain * `update`). Apply uses this to route writes to the right admin action. */ versionBoundFields?: string[]; /** * True when the desired automation declares a `reaction_script` — server stores * it write-only, so the diff can't tell whether it changed; apply always * re-pushes (idempotent). Matches the auth-profile credentials pattern. * An explicit `null` (reaction removal) also counts as declared; apply * clears it via `set_reaction_script`. */ reactionScriptDeclared?: boolean; } export interface ConnectorDefinitionDiffRow extends BaseRow { kind: "connector-definition"; desired?: DesiredConnectorDefinition; /** * Whether the desired connector is currently installed remotely. When the * connector key isn't known up front (a local `.ts` the server hasn't * compiled), this is `false` and the verb is "create" — `install_connector` * is idempotent and reports `updated: false` on apply if nothing changed. */ installedRemotely?: boolean; } export interface AuthProfileDiffRow extends ResourceRow { kind: "auth-profile"; /** True for `oauth_account` / `browser_session` profiles not yet `active`. */ needsAuth?: boolean; } /** * Field names `diffConnection` and `diffFeed` can report. `changedFields` is * the sole input to apply-cmd routing, and apply reads these exact strings back * to decide what goes in the update payload. Naming them here means a rename or * a typo on EITHER side fails the build, instead of silently dropping the field * from the wire and leaving the remote value stale. */ export interface ConnectionDiffRow extends ResourceRow { kind: "connection"; changedFields?: ConnectionField[]; } export interface FeedDiffRow extends ResourceRow { kind: "feed"; /** Owning connection slug. */ connectionSlug: string; changedFields?: FeedField[]; } export interface InferenceProviderDiffRow extends ResourceRow { kind: "inference-provider"; /** * True when the desired provider declares an API key — the server stores it * write-only (can't be read back), so the diff can't tell whether it changed; * apply always re-pushes (idempotent). Matches the auth-profile credentials / * automation reaction-script pattern. */ keyDeclared?: boolean; /** * Modalities whose capability block differs from remote (or is new). Apply * PUTs each. On a create, every declared modality is listed. */ capabilityModalities?: InferenceModality[]; } /** * A blocking drift item: either a field moved remotely or a remote-only * definition is not safe to delete. Apply must not converge over it — the * whole run blocks and reports this. */ export interface BlockingDriftRow extends BaseRow { verb: "drift"; /** Always true — distinguishes blocking drift from legacy non-blocking drift. */ blocking: true; kind: "entity-type" | "relationship-type" | "automation"; id: string; /** The moved field; absent for a whole remote-only definition. */ field?: string; /** The remote value, or display metadata for a remote-only definition. */ remoteChange?: unknown; /** What the config declares for that field — what apply would have written. */ desiredChange?: unknown; } /** * The attribution baseline: what the remote looked like after the last * succeeded apply, plus the set of definition incarnation identities * (`${kind}:${id}`) this config actually applied. Drives "who moved". */ export interface AttributionSnapshot { entityTypes: RemoteEntityType[]; relationshipTypes: RemoteRelationshipType[]; automations: RemoteAutomation[]; } export interface Baseline { /** * Whether the manifest contained attribution and ownership. An absent * baseline permits two-way convergence for declared definitions but never * provenance-dependent deletion. */ recorded: boolean; attribution: AttributionSnapshot; /** `${kind}:${id}` — delete-eligible definition incarnation identities. */ owned: Set; /** * Connector key → version recorded after the last succeeded apply (from the * deployment manifest's `connector_versions`). Used to refuse config-expressed * connector deletes when the remote incarnation was edited post-baseline. */ connectorVersions?: Record; } export declare const ownedKey: (kind: string, id: number | string | undefined) => string; export type DiffRow = AgentDiffRow | SettingsDiffRow | EntityTypeDiffRow | RelationshipTypeDiffRow | AutomationDiffRow | ConnectorDefinitionDiffRow | AuthProfileDiffRow | ConnectionDiffRow | FeedDiffRow | InferenceProviderDiffRow | BlockingDriftRow; export interface DiffPlan { rows: DiffRow[]; /** Aggregate counters for the summary line. */ counts: { create: number; update: number; noop: number; drift: number; /** * Definitions absent from the config that apply will delete. Always 0 * unless the config declares prune (`computeDiff({ prune: true })`); * otherwise those remote-only definitions are reported as `drift`. */ delete: number; }; /** * Informational, non-actionable notes — e.g. "connector X is installed * remotely but not declared locally". Rendered after the plan; never block * apply. */ notes: string[]; } /** * Deterministic serialization (sorted keys, undefined dropped). Exported for * the deployment manifest hash (deployment.ts), which must be stable across * key-insertion order. */ export declare function canonical(value: unknown): string; /** * One updatable field of a resource: how to tell whether it changed, and how it * appears in the update payload. Keeping both halves in one entry is the point * — the diff's field list and the wire payload used to be two hand-maintained * lists that had to agree by eye, and they silently disagreed for `schedule` * and `config`. */ interface FieldSpec { changed: (desired: D, remote: R) => boolean; /** The payload fragment this field contributes when it HAS changed. */ payload: (desired: D) => Partial

; } /** * A resource's field table. The payload type `P` is carried on the table's own * type rather than inferred from the entries, so {@link buildUpdatePayload} * returns the real payload shape instead of whichever fragment happens to be * declared first. */ interface FieldTable { readonly fields: Record>; } /** * Every updatable connection field. `ConnectionField` is derived from this * object's keys, so the union and the table cannot drift: adding a key here * adds it to the union, and there is nowhere else to add one. * * BYO chat connections never reach `updateConnection` (apply routes them to the * secret-aware chat-upsert), so the BYO branches below only govern diffing. */ declare const CONNECTION_FIELDS: FieldTable; /** Every updatable feed field. See {@link CONNECTION_FIELDS}. */ declare const FEED_FIELDS: FieldTable; export type ConnectionField = keyof typeof CONNECTION_FIELDS.fields; export type FeedField = keyof typeof FEED_FIELDS.fields; /** * Builds an update payload from the diff's changed-field list: a field the * config does not declare produces no changed-field, so it never appears as a * key and the server leaves it alone. */ export declare function buildUpdatePayload(table: FieldTable, changedFields: readonly F[] | undefined, desired: D): Partial

; /** The connection and feed field tables, for apply to build payloads from. */ export declare const UPDATE_FIELD_TABLES: { readonly connection: FieldTable; readonly feed: FieldTable; }; /** * The entity-type facets a comparison cares about, normalized so an absent * collection and an empty one compare equal. */ export interface EntityTypeFacets { name: string | undefined; description: string | undefined; required: string[]; properties: Record; backing: unknown; metrics: unknown; eventKinds: unknown; viewTemplate: unknown; schemaExtras: Record; } /** * What the remote entity type BECOMES after this apply: declared values, plus * the facets the write provably leaves alone — name/description (upsert never * clears them), and, outside prune, every omitted facet including remote-only * property keys (`upsertEntityType` merges them back). Under prune an omitted * clearable facet is removed, so it projects to its cleared value. * * Single source for BOTH the recorded attribution baseline and the * "already in sync?" test that lets an existing org establish its first * baseline: a second, prune-blind projection is what made prune-managed * eventKinds/viewTemplate/backing look inherited and recorded a phantom noop. */ export declare const effectiveEntityTypeAfterApply: (d: DesiredEntityType, r: RemoteEntityType | undefined, prune: boolean) => EntityTypeFacets; export interface RelationshipTypeFacets { name: string | undefined; description: string | undefined; rules: unknown[]; } /** * What the remote relationship type BECOMES after this apply. name/description * are never cleared by omission (upsert only sets what is declared); omitted * `rules` ARE cleared — `upsertRelationshipType` writes `[]`. */ export declare const effectiveRelationshipTypeAfterApply: (d: DesiredRelationshipType, r: RemoteRelationshipType | undefined) => RelationshipTypeFacets; interface AutomationProjection { executor?: unknown; agent?: string | null; name?: string | null; description?: string | null; triggers?: DesiredAutomationTrigger[]; prompt?: string | null; skills?: Array<{ name: string; content: string; }> | null; sources?: AutomationSource[] | null; reactionsGuidance?: string | null; deviceWorkerId?: string | null; model?: string | null; minCooldownSeconds?: number | null; tags?: string[] | null; agentKind?: string | null; outputs?: Record | null; classifiers?: unknown[] | null; } /** * Desired Automation → projection. For fields the two-way diff only compares * when declared, an omitted value inherits the live remote (unmanaged) so * three-way attribution never false-blocks on preserved remote state. * Always-managed fields (`triggers`, `prompt`, `outputs`, `agent`, `name`) * use the same defaults as `diffAutomation`. */ export declare const projectDesiredAutomation: (d: DesiredAutomation, remote?: RemoteAutomation) => AutomationProjection; export interface RemoteSnapshot { agents: RemoteAgent[]; /** keyed by agentId */ agentSettings: Map; entityTypes: RemoteEntityType[]; relationshipTypes: RemoteRelationshipType[]; automations: RemoteAutomation[]; connectorDefinitions: RemoteConnectorDefinition[]; authProfiles: RemoteAuthProfile[]; connections: RemoteConnection[]; /** Feeds keyed by connection ID. */ feedsByConnectionId: Map; /** Org-owned inference providers (from `GET /inference-providers`). */ inferenceProviders: RemoteInferenceProvider[]; } /** * The subset of {@link DesiredState} the diff consumes — everything except the * apply-only knobs (prune flag, org metadata, requiredSecrets). */ type DesiredStateForDiff = Pick; interface ComputeDiffOptions { /** Limit the diff to a subset of resource kinds. */ only?: "agents" | "memory"; /** * When true, the config declares `prune: true`: it's the source of truth for * *definitions*, so a remote definition (entity type, relationship type, * automation, connector definition) absent from desired is emitted as a `delete` * row instead of an ignored `drift` — INCLUDING definitions created via the * dashboard/API. Data (entity/relationship instances), connections, auth * profiles, feeds, and agents are never pruned. Default (false) * reports those remote-only definitions as `drift`. */ prune?: boolean; /** * Target org id. The entity/relationship-type list endpoints also return * *public* definitions owned by OTHER orgs, which this org neither manages * nor can delete — so a remote type whose `organization_id` differs is * excluded from drift/delete entirely. Omit to disable the filter (tests). */ orgId?: string; /** * Attribution baseline (the last succeeded deployment's effective-remote * snapshot + owned incarnation identities). When recorded, entity/rel types * and Automations get the three-way compare: block when the remote moved * (`remote ≠ attribution` AND `desired ≠ remote`), converge only when the * config moved (`remote == attribution`). Without a recorded baseline, * declared definitions use the ordinary two-way diff; remote-only prune * candidates block because ownership cannot be proven. */ baseline?: Baseline; } export declare function computeDiff(desired: DesiredStateForDiff, remote: RemoteSnapshot, opts?: ComputeDiffOptions): DiffPlan; export {}; //# sourceMappingURL=diff.d.ts.map