/** * The scai ↔ orchestrator **sync contract** — the single, versioned * source of truth for the typed surface scai emits under `--json` for * push / pull / diff, and the capability handshake it answers. * * ## Why this exists * * scai is spawned as a subprocess by the orchestrator. Historically the * orchestrator parsed scai's human-readable stdout with regexes * (`/not found/`, `/does not exist yet/`), read resolved Sitecore UUIDs * from a side-channel file (`--identities-out`), and gated newer flags * behind `SCAI_HAS_*` env booleans. The registry, one hop further out, * regexed the error string (`/conflict|cms-edit|policy_denied/`) to * decide whether to show the conflict-resolution UI. Every contract * change was a three-repo lockstep with no compile- or run-time signal * when the lockstep broke — so drift surfaced at runtime, per entity, in * production. * * The irony the audit surfaced: scai already produces this data in typed * form. `classifyHashes` is shared, conflicts already throw a typed * `ScaiError(POLICY_DENIED)`, per-cell `classification` already rides on * `RecipeChange.meta`, and `ApplyResult` already carries `identities`. * The only gap was that the push/pull *success* path flattened all of it * to text at the CLI boundary. This module closes that gap: it defines * the typed shapes, and {@link buildSyncResult} projects scai's existing * outcome objects into them. * * ## The contract * * Downstream consumers (orchestrator, registry) **mirror** the `*Schema` * Zod validators in their own repos and check {@link SYNC_CONTRACT_VERSION} * at the process boundary. A version they don't recognize is a loud, * fail-fast error — never a silent degrade. We deliberately do NOT ship a * shared published package today (scai is a closed binary; the consumers * already mirror recipe schemas the same way). If scai later exports this * as a stable subpath, the mirrors collapse into an import — a mechanical * change that the `contractVersion` guard makes safe. * * Bump {@link SYNC_CONTRACT_VERSION} only when a consumer that validates * against the *old* version would mis-handle the *new* payload. Additive, * optional fields don't need a bump; renames / removals / semantic * changes do. */ import { z } from "zod"; import type { PushOutcome, SyncMode } from "./engine.js"; import type { KindRef, ResolvedIdentity } from "./kind.js"; /** * Contract version. Bump on a breaking change to any shape below. * Consumers compare against this and refuse to proceed on a mismatch. */ export declare const SYNC_CONTRACT_VERSION: "1"; export type SyncContractVersion = typeof SYNC_CONTRACT_VERSION; /** Mirror of `FieldClassification` (sync/baseline) as a Zod enum. */ export declare const FieldClassificationSchema: z.ZodEnum<{ "cms-edit": "cms-edit"; conflict: "conflict"; "first-push": "first-push"; "recipe-change": "recipe-change"; }>; /** Mirror of `ChangeKind` (sync/plan) as a Zod enum. */ export declare const ChangeKindSchema: z.ZodEnum<{ delete: "delete"; create: "create"; update: "update"; noop: "noop"; }>; /** * One change in a serialized plan. A pared-down, wire-stable projection * of `RecipeChange` — the kind-private `meta` bag is dropped; only the * per-cell `classification` is lifted out of it because that's the one * piece a consumer needs to render "this cell had a tenant edit". */ export declare const SyncChangeSchema: z.ZodObject<{ kind: z.ZodEnum<{ delete: "delete"; create: "create"; update: "update"; noop: "noop"; }>; path: z.ZodString; summary: z.ZodString; classification: z.ZodOptional>; before: z.ZodOptional; after: z.ZodOptional; }, z.core.$strip>; export type SyncChange = z.infer; /** * A cell where the tenant diverged from baseline — the only thing a * consumer needs to decide whether to surface the conflict-resolution * affordance. Under `error` policy these BLOCK the push (they ride in * {@link SyncErrorSchema}); under `cms-wins` / `recipe-wins` they were * resolved and ride in {@link SyncResultSchema} as informational. */ export declare const SyncConflictCellSchema: z.ZodObject<{ path: z.ZodString; classification: z.ZodEnum<{ "cms-edit": "cms-edit"; conflict: "conflict"; }>; }, z.core.$strip>; export type SyncConflictCell = z.infer; /** Mirror of `ResolvedIdentity` (sync/kind). Replaces `--identities-out`. */ export declare const ResolvedIdentitySchema: z.ZodObject<{ scope: z.ZodEnum<{ "brand-kit": "brand-kit"; brief: "brief"; campaign: "campaign"; deliverable: "deliverable"; task: "task"; }>; handle: z.ZodOptional; name: z.ZodOptional; parentHandle: z.ZodOptional; parentName: z.ZodOptional; sitecoreId: z.ZodString; }, z.core.$strip>; /** The instance the operation targeted. Mirror of the public bits of `KindRef`. */ export declare const SyncRefSchema: z.ZodObject<{ id: z.ZodString; baselineKey: z.ZodOptional; tenantId: z.ZodOptional; }, z.core.$strip>; export declare const PlanSummarySchema: z.ZodObject<{ create: z.ZodNumber; update: z.ZodNumber; delete: z.ZodNumber; noop: z.ZodNumber; }, z.core.$strip>; /** * The typed `data` payload carried in a `ScaiEnvelope` under `--json` for * every sync push / pull / diff. This is what replaces stdout regexing. */ export declare const SyncResultSchema: z.ZodObject<{ contractVersion: z.ZodLiteral<"1">; operation: z.ZodEnum<{ push: "push"; diff: "diff"; pull: "pull"; }>; kind: z.ZodString; ref: z.ZodObject<{ id: z.ZodString; baselineKey: z.ZodOptional; tenantId: z.ZodOptional; }, z.core.$strip>; mode: z.ZodEnum<{ "what-if": "what-if"; apply: "apply"; }>; plan: z.ZodArray; path: z.ZodString; summary: z.ZodString; classification: z.ZodOptional>; before: z.ZodOptional; after: z.ZodOptional; }, z.core.$strip>>; summary: z.ZodObject<{ create: z.ZodNumber; update: z.ZodNumber; delete: z.ZodNumber; noop: z.ZodNumber; }, z.core.$strip>; applied: z.ZodNumber; skipped: z.ZodNumber; conflicts: z.ZodArray; }, z.core.$strip>>; identities: z.ZodArray; handle: z.ZodOptional; name: z.ZodOptional; parentHandle: z.ZodOptional; parentName: z.ZodOptional; sitecoreId: z.ZodString; }, z.core.$strip>>; }, z.core.$strip>; export type SyncResult = z.infer; /** * The typed `data` payload scai emits under `--json` when a sync op * FAILS. A superset of the generic CLI error JSON (see `cli.ts`) with a * structured `conflicts[]` so a `POLICY_DENIED` three-way block is * machine-routable without parsing `details` strings. */ export declare const SyncErrorSchema: z.ZodObject<{ contractVersion: z.ZodLiteral<"1">; code: z.ZodString; message: z.ZodString; hint: z.ZodOptional; details: z.ZodOptional>; conflicts: z.ZodOptional; }, z.core.$strip>>>; remediation: z.ZodOptional; fix: z.ZodString; detail: z.ZodOptional; }, z.core.$strip>>; exitCode: z.ZodNumber; }, z.core.$strip>; export type SyncError = z.infer; /** * Stable feature tokens advertised by `scai capabilities`. The * orchestrator reads these ONCE at spawn and branches on the set, * replacing the scatter of `SCAI_HAS_*` env booleans. Add a token here * when you ship a capability the orchestrator needs to detect; never * remove one without a contract-version bump. */ export declare const SYNC_FEATURES: readonly ["json-sync-result", "identities-in-envelope", "capabilities", "conflict-policy", "structured-conflicts", "campaign-pull-sitecore-id", "brief-pull-sitecore-id", "campaign-pull-handle", "list-lean"]; export type SyncFeature = (typeof SYNC_FEATURES)[number]; /** * Recipe kinds that participate in the typed sync contract. The * orchestrator reads this off the capability handshake to know which * kinds it can drive through the `--json` envelope. */ export declare const SYNC_CONTRACT_KINDS: readonly ["brand-kit", "brief", "brief-type", "campaign"]; /** Conflict policies the contract accepts, per direction. */ export declare const SYNC_CONFLICT_POLICIES: { readonly push: readonly ["error", "recipe-wins", "cms-wins"]; readonly pull: readonly ["error", "recipe-wins", "tenant-wins"]; }; /** * The capability handshake `scai capabilities --json` answers. The * orchestrator validates `contractVersion`, then gates behaviour on the * `features` set instead of probing flags via env. */ export declare const SyncCapabilitiesSchema: z.ZodObject<{ contractVersion: z.ZodLiteral<"1">; scaiVersion: z.ZodString; features: z.ZodArray; kinds: z.ZodArray; conflictPolicies: z.ZodObject<{ push: z.ZodArray; pull: z.ZodArray; }, z.core.$strip>; }, z.core.$strip>; export type SyncCapabilities = z.infer; /** * Build a {@link SyncResult} from a `syncPush` outcome. The single place * scai's internal outcome shape becomes the wire contract — keep the * projection here so the command surfaces stay thin. */ export declare const buildSyncResult: (params: { operation: SyncResult["operation"]; kind: string; ref: KindRef; mode: SyncMode; outcome: PushOutcome; }) => SyncResult; /** * Build a {@link SyncResult} for a pure pull (`syncPull` returns a recipe, * not a plan). A pull writes the tenant state into the recipe side, so * there's no remote write to report — `plan` is empty and * `applied`/`skipped` are 0. Whether an instance was found on the tenant * is surfaced by the command via `envelope.meta.found`, keeping push and * pull `SyncResult` bodies symmetric. */ export declare const buildPullResult: (params: { kind: string; ref: KindRef; identities?: ResolvedIdentity[]; }) => SyncResult;