import type { OperationIr } from "../ir/operations.js"; import type { ComponentTemplateRecipeParsed, ContentItemRecipeParsed, ContentTemplateRecipeParsed, PageRecipeParsed, PageTemplateRecipeParsed } from "../schema/recipe.js"; /** Per-recipe merge classification (see runRecipePull JSDoc). */ export type RecipeMergeStatus = "in-sync" | "disk-ahead" | "tenant-edited" | "conflict" | "disk-only" | "tenant-only"; /** * Classify one recipe's merge state from per-(itemRefKey, fieldKey) * hash maps for disk vs tenant vs baseline. Direction-inverted from * push's drift classification: * - disk == baseline + tenant != baseline → `tenant-edited` * - disk != baseline + tenant == baseline → `disk-ahead` * - both diverged, disk != tenant → `conflict` * - one side missing entirely → `disk-only` / `tenant-only` * - everything equal → `in-sync` */ export declare const classifyMergeStatus: (diskHashes: Map | null, tenantHashes: Map | null, baselineHashes: Map | null) => { status: RecipeMergeStatus; diskChanged: number; tenantChanged: number; }; /** One per-field classification — same domain as the per-recipe rollup. */ export type FieldMergeStatus = "in-sync" | "disk-ahead" | "tenant-edited" | "conflict"; /** * Convert an internal baselineLookupKey (`itemref|name:foo|lang|version`) * to a human-readable label for CLI output. Drops the itemRefKey segment * (always the same per recipe) and renders `(lang, v#)` only when set. */ export declare const humanizeFieldKey: (key: string) => string; /** * Per-field classification map. Same key shape as `indexHashes` — encodes * `(itemRefKey, fieldName|fieldId, language, version)` so a Recipe-side * field name can be hashed back to a status lookup. */ export type PerFieldStatuses = Map; /** * Per-field three-way classification — fans the per-(itemRefKey, fieldKey) * decision out so the caller can act per-field (e.g. synthesise a merged * Recipe under `tenant-wins` that preserves `disk-ahead` fields). * * disk == tenant → in-sync * disk != tenant && tenant == baseline → disk-ahead * disk != tenant && disk == baseline → tenant-edited * both != baseline (or no baseline) → conflict */ export declare const perFieldStatuses: (diskHashes: Map | null, tenantHashes: Map | null, baselineHashes: Map | null) => PerFieldStatuses; /** * Roll up per-field statuses into a single per-recipe status. Mirrors * the legacy `classifyMergeStatus` behaviour for the per-recipe summary * (now derived from the same per-field map so the two views stay * consistent). */ export declare const rollupPerFieldStatuses: (statuses: PerFieldStatuses) => { status: RecipeMergeStatus; diskChanged: number; tenantChanged: number; }; /** * Per-field merge for content-value-bearing recipes (ContentItem, Page). * Under `tenant-wins`: * - `disk-ahead` fields (local change unique to disk) → KEEP disk value * - all other statuses → take tenant value * Fields present on only one side adopt that side's value verbatim. * * The base structure (handle, name, displayName, description, template, * workflow) comes from tenant — those aren't per-field tracked. * * Layout is per-(language, version) merged like other fields: the * baseline hash for `__Renderings` / `__Final Renderings` is computed * against the canonicalised XML form (`canonicaliseLayoutXml` in * baseline.ts, which parses through `parseLayoutXml` then serialises * to deterministic JSON), so the same logical layout hashes identical * regardless of canonical-vs-SXA-delta wire form. `layoutWinner` * inside this merge picks disk over tenant when the per-(lang, version) * cell is `disk-ahead`, else tenant. Applies to: * - per-version layouts on both ContentItem + Page (`versions[][n].layout`) * - Page item-level `recipe.layout` (simple mode) via the * (DEFAULT_LANGUAGE, DEFAULT_VERSION) cell * * The function returns the tenantRecipe unchanged when `mainItemRefKey` * couldn't be located (defensive: a degenerate IR with no main * CreateItem op shouldn't crash the pull). */ export declare const mergeContentValueRecipe: (diskRecipe: ContentItemRecipeParsed | PageRecipeParsed, tenantRecipe: ContentItemRecipeParsed | PageRecipeParsed, statuses: PerFieldStatuses, mainItemRefKey: string | undefined, /** * Optional per-(rawKey) override for the winner decision. Operator-set * via the merge-plan file (`--apply-plan`). When a key is present in * the overrides map, its value is used directly; missing keys fall * through to the default `disk-ahead → disk, else → tenant` policy. */ winnerOverrides?: Map) => ContentItemRecipeParsed | PageRecipeParsed; /** * Per-field merge for template-style recipes (ComponentTemplate, * ContentTemplate, PageTemplate). Matches `recipe.fields[]` (and * `recipe.params[]` for ComponentTemplate) by field NAME — names are * the stable identity that survives reordering. Per-field rollup picks * the winning side at the FieldDefinition level (not per-property); * disk-ahead fields preserve their disk definitions, everything else * (in-sync, tenant-edited, conflict, no-data) yields to tenant. * * Limitations (documented in code): * - Renames are detected as delete + create (old name → tenant-only, * new name → disk-only); not auto-reconciled. * - Per-property sub-merge (e.g. shape from disk, source from tenant * within the same field) is intentionally NOT done — it'd risk * producing a structurally-malformed FieldDefinition (e.g. a * shape:image with non-image source). Field is the unit of merge. * - `variants[]`, `placeholders[]`, `placedIn[]`, `meta`, `datasource` * are taken from the tenant base for ComponentTemplate — those have * their own merge semantics that this MVP doesn't tackle. * * Returns the tenantRecipe untouched when the IR maps couldn't be * built (defensive). */ export declare const mergeTemplateRecipe: ({ diskRecipe, tenantRecipe, statuses, diskIr, tenantIr, winnerOverrides, }: { diskRecipe: ComponentTemplateRecipeParsed | ContentTemplateRecipeParsed | PageTemplateRecipeParsed; tenantRecipe: ComponentTemplateRecipeParsed | ContentTemplateRecipeParsed | PageTemplateRecipeParsed; statuses: PerFieldStatuses; diskIr: OperationIr; tenantIr: OperationIr; /** Per-(rawKey) override for the winner decision (merge-plan file). */ winnerOverrides?: Map; }) => ComponentTemplateRecipeParsed | ContentTemplateRecipeParsed | PageTemplateRecipeParsed; /** * Roll per-property statuses up to one entry per template field. The * underlying IR emits multiple SetField ops per template-field item * (Type, Source, Title, SortOrder, Shared, ...), so the raw * `PerFieldStatuses` map has multiple entries per recipe-author-visible * field. The merge-plan + the `mergeTemplateRecipe.winnerFor` lookup * both operate at the *field* level (bare fieldRefKey); without this * rollup the plan's per-property keys never match the lookup's bare * keys and overrides are silently dropped (audit B1). * * Returns `{ statuses, labels }` so composeMergePlan can pre-fill * winners with the right rollup AND show "Title" instead of "type" in * the plan file (audit N1). */ export declare const rollupTemplateStatuses: (fieldStatuses: PerFieldStatuses, ir: OperationIr) => { statuses: PerFieldStatuses; labels: Map; };