/** * Shared leaf helpers for the reverse-projection (`read-current`). * * GUID/field accessors, handle recovery, sort ordering, the Sitecore * field-type ⇄ abstract-shape inverse maps, single-field reverse-projection, * the per-template field-shape walker, the GUID→handle marker index, and the * layout-XML helpers. These are the cohesive utilities every per-kind * projector and walker shares — pure functions plus a couple of async * client-driven walks, no per-kind recipe knowledge. * * See `../read-current.ts` for the module-level contract. */ import type { AuthoringApiClient, RemoteItem } from "../../api/client.js"; import { type ParsedPlacement } from "../../layout/parse.js"; import type { ComponentPlacement, FieldDefinition, Layout } from "../../schema/recipe.js"; import type { FieldShape, SitecoreFieldType } from "../../schema/field-types.js"; /** * Normalise a Sitecore GUID for comparison: lowercase, strip curly braces * and hyphens. The Authoring API returns GUIDs hyphen-less * (`1930bbeb7805471a…`) while the built-in template constants are * hyphenated — normalising both sides to the bare 32-hex form is what makes * `conformsTo` / `guidEquals` actually match against a live tenant. */ export declare const normalizeGuid: (guid: string) => string; /** True when two Sitecore GUIDs refer to the same item (curly/case-insensitive). */ export declare const guidEquals: (a: string | undefined, b: string | undefined) => boolean; /** * Look up a field value on a `RemoteItem` by field GUID OR field name. The * compiler emits some fields by GUID and some by name; reverse-projection * matches on either so it stays robust against the GUID/name split the * executor's resolver papers over (see `RemoteFieldValue.name`). */ export declare const fieldValue: (item: RemoteItem, fieldId: string, fieldName?: string) => string | undefined; /** Find a field value by field NAME only (case-insensitive). */ export declare const fieldValueByName: (item: RemoteItem, fieldName: string) => string | undefined; /** True when the item conforms to the given Sitecore built-in template. */ export declare const conformsTo: (item: RemoteItem, templateId: string) => boolean; /** * Recover an item's recipe handle — its stable identity. * * Recovery rule: * 1. Tenant-stamped `Scai Handle` marker (preferred, but TRUSTED ONLY * IF SHAPE-VALID — see security note below). * 2. Synthesise from the item name (fallback for unmarked items). * * Prefers the `Scai Handle` marker field, which carries the *exact* handle * `push` stamped on every recipe-managed item: a marked item round-trips to * the author's real handle regardless of how the item was later moved or * renamed. Falls back to synthesising one from the item name * (`handleFromName`) only for unmarked items — a first capture of an * environment scai never pushed to, or items created outside scai. * * SECURITY: the `Scai Handle` field is tenant-controlled — any author * with write access to the item can set it to an arbitrary string. * Downstream consumers (`writeRecipeJson`, `FileBaselineStorage.locator`) * fold the handle into a filesystem path via `slugifyHandle`, which only * replaces `@` with `_v` — it does NOT strip path separators or `..` * segments. A malicious handle like `"../../tmp/pwn@1"` would resolve * outside the operator's output directory. * * Defence: validate the marker against `HANDLE_PATTERN` * (`/^[a-z][a-z0-9-]*@[0-9]+$/`) before trusting it. The pattern * forbids `/`, `\`, `.`, leading dot, uppercase — so a tampered marker * is rejected and we fall back to `handleFromName`, which builds a * deterministic kebab handle from the item's Sitecore name (also * subject to Sitecore's own item-naming rules; safe). * * See `marker.ts` and docs/recipe-sync-architecture.md, "Recipe identity". */ export declare const handleOf: (item: RemoteItem) => string; /** Stable child ordering: Sitecore sort order, then name as a tiebreak. */ export declare const byTreeOrder: (a: RemoteItem, b: RemoteItem) => number; export declare const sitecoreTypeFromLabel: (label: string) => SitecoreFieldType | undefined; /** * Map a stored Sitecore field type back to the recipe's abstract * `FieldShape`. Falls back to `"text"` for any unmapped value so an unknown * type never crashes the read path. */ export declare const shapeFromSitecoreType: (type: SitecoreFieldType) => FieldShape; /** * Reverse-project a single `TEMPLATE_FIELD` item into a `FieldDefinition`. * * Faithful: field `name`, the Sitecore `Type` (carried verbatim on * `sitecore.type`), the section it lives under (`sitecore.section`), * `sitecore.sortOrder`, and the storage axis (`sitecore.storage`, recovered * from the field's `Shared` / `Unversioned` flags). The `Source` value is * preserved verbatim via `sitecore.source = { kind: "raw", value }` — * the structured `filter` decomposition (`types`/`query`/`scope`) is * intentionally NOT reverse-engineered (it would require parsing the * URL-encoded Source and resolving GUIDs back to recipe handles); * `kind: "raw"` round-trips to the identical wire string. * * LOSSY / omitted: `required`, `hint`, `default`, `enumHandle`, and the * abstract `multiple` flag are not recoverable from a field item alone and * are omitted. The abstract `shape` is a best-effort inverse of the stored * `Type` — see `shapeFromSitecoreType`. */ export declare const fieldFromItem: (fieldItem: RemoteItem, sectionName: string) => FieldDefinition; /** * Walk a template item's `TEMPLATE_SECTION` children and reverse-project * every `TEMPLATE_FIELD` leaf under them into ordered `FieldDefinition`s. * * `__Standard Values` children are skipped — they're not sections. Sections * and fields are emitted in Sitecore sort order so the round-trip preserves * authored ordering. */ export declare const fieldsOfTemplate: (templateItem: RemoteItem, client: AuthoringApiClient) => Promise; /** True when a template item carries the SXA component base templates. */ export declare const hasSxaComponentBases: (templateItem: RemoteItem) => boolean; /** * True when a template item carries the SXA Headless page base set — * the marker that classifies it as a `page-template` rather than a * plain content template. Disjoint from `hasSxaComponentBases`: * components inherit datasource/component bases, pages inherit the * Base Page / navigation / designable / sitemap facets. */ export declare const hasSxaPageBases: (templateItem: RemoteItem) => boolean; /** * Per-field decoder metadata: the abstract shape and the storage axis the * template declares. The walker uses `storage` to bucket field values into * `shared` vs. per-(language, version) cells before round-tripping. */ export interface TemplateFieldInfo { shape: FieldShape; storage: "shared" | "unversioned" | "versioned"; } /** `lowercase(fieldName) → TemplateFieldInfo` for one Sitecore template. */ export type TemplateFieldShapes = Map; /** * Walk a template item's sections + fields and return a * `lowercase(fieldName) → {shape, storage}` map — every field the template * declares, plus every field its base templates declare (recursively). * * The cache short-circuits repeat walks: a content item references its * template by GUID, and many items typically share a template, so the * per-(`templateGuid`) lookup pays the walk cost once per template. * * Returns an empty map when the template item can't be loaded — the caller * then falls back to inferring per-field shapes from the wire value, which * may drop ambiguous fields rather than guess. */ export declare const getTemplateFieldShapes: (templateGuid: string, client: AuthoringApiClient, cache: Map) => Promise; /** True for any field the recipe surface considers authorable. */ export declare const authorableFieldsOf: (item: RemoteItem) => RemoteItem["fields"]; /** * GUID → recipe-handle index for resolving layout-XML references. * * Layout `` elements reference renderings and datasource items by raw * Sitecore GUID; recipes reference the same things by `handle@major`. This * index is the bridge: every entry comes from an item's `Scai Handle` * marker (see `marker.ts`), keyed by the item's normalised GUID. * * A GUID with no marker is genuinely unrecoverable — there is no name to * synthesise a handle from for a *layout reference* (the layout XML carries * only the GUID, not the target item's name), so an unindexed GUID is * dropped at resolution time rather than fabricated. This is the * lossy-projection contract: omit, never invent. */ export type GuidHandleIndex = Map; /** * Walk a content-tree subtree collecting every item's `Scai Handle` marker * into a GUID→handle map. Recurses through *all* children — the renderings * tree nests renderings under section folders, the content tree nests page * items and datasource items arbitrarily deep. * * Returns silently (contributing nothing) when `rootPath` resolves to no * item — an absent root is not an error, just an empty contribution. */ export declare const indexMarkersUnder: (rootPath: string | undefined, client: AuthoringApiClient, index: GuidHandleIndex) => Promise; /** * Resolve one `ParsedPlacement` (a decoded layout `` element) into a * recipe-level `ComponentPlacement`, or `null` when the placement's * rendering GUID can't be resolved to a handle. * * Faithful: `variant`, `params`, and — for a `local:` sentinel — the * `scoped` slot, all of which the layout XML carries directly. * * LOSSY: * - the rendering GUID MUST resolve via the marker index; an unindexed * GUID drops the whole placement (returning `null`) — there is no * recoverable handle, and a fabricated one would derive the wrong * `renderingId` on the next push. * - a `ds` GUID that resolves becomes a `kind: "shared"` datasourceRef; * one that doesn't is omitted (the placement keeps its variant/params * but loses the datasource binding — better than a dangling handle). * Distinguishing a `shared` content-item GUID from a `scoped` page-local * one is not attempted: `readCurrent` does not reverse-project page-tree * datasource items, and a resolved `ds` handle is treated as `shared`. * A `local:` sentinel is the one unambiguous `scoped` signal. */ export declare const placementFromParsed: (parsed: ParsedPlacement, guidIndex: GuidHandleIndex) => ComponentPlacement | null; /** * Reverse-project a layout XML string into a recipe-level `Layout`. * * Parses the XML (`parseLayoutXml` — handles both canonical + delta wire * forms), then resolves every placement's GUIDs to handles through the * marker index. Placements whose rendering GUID is unresolvable are * dropped; a placeholder left with no placements after the drop is * omitted entirely. Per-placeholder placement order is preserved. */ export declare const layoutFromXml: (xml: string, guidIndex: GuidHandleIndex) => Layout; /** Read an item's layout XML — `__Renderings` (shared) field. */ export declare const sharedLayoutXmlOf: (item: RemoteItem) => string; /** Read an item's final layout XML — `__Final Renderings` (versioned) field. */ export declare const finalLayoutXmlOf: (item: RemoteItem) => string; /** Read the per-version `__Final Renderings` layout XML and decode to a Layout. */ export declare const layoutOfSnapshot: (snapshot: RemoteItem, guidIndex: GuidHandleIndex) => Layout | undefined;