/**
 * Convert an arbitrary string into a URL-safe slug: lowercase ASCII, with words
 * separated by single dashes and no leading/trailing dashes.
 *
 * Slugs are filenames in nanoesis (DESIGN §5.3), this function is the single
 * definition of what a valid URL segment looks like, shared by the rename UI and
 * the compiler so the editor and the engine can never disagree.
 */
declare function slugify(input: string): string;

/**
 * Turn a token name into a human label for the editor (DESIGN §6.1):
 * `hero_image` → "Hero image". Separators become spaces, camelCase is split, and
 * the result is sentence-cased.
 */
declare function humanize(name: string): string;

/** Best-effort content type from a path's extension; defaults to octet-stream. */
declare function contentTypeFor(path: string): string;

/** Thrown when raw content JSON cannot be coerced into a {@link ContentItem}. */
declare class ContentParseError extends Error {
    constructor(message: string);
}
/**
 * Parse-time diagnostics that survive into the validation gate. Distinct from
 * {@link ContentParseError} (which signals a *malformed* file) — diagnostics describe
 * a successfully parsed item that nonetheless carried something the parser dropped.
 */
interface ContentParseDiagnostics {
    /**
     * Top-level keys present in the JSON but not recognised by the parser. Sorted for
     * deterministic message ordering. Empty when the file is clean. Typical cause: an
     * author (especially an LLM) wrote a content field (`body`, `tagline`, ...) at the
     * top level instead of nesting it under `fields`, or typo'd a system key
     * (`IsPublished` instead of `isPublished`).
     */
    readonly unknownTopLevelKeys: readonly string[];
}
/**
 * Validate untrusted content JSON into a typed {@link ContentItem}, the single
 * boundary where content data is checked (CLAUDE.md §4). Tolerant of drift
 * (unknown keys ignored, missing isPublished/fields defaulted; DESIGN §5.4) but
 * strict on the system keys that carry meaning.
 *
 * The thin wrapper around {@link parseContentItemWithDiagnostics} for the call
 * sites that don't need to know what the parser dropped (the editor, tests, …).
 * The validation gate uses the diagnostics-returning variant so it can warn about
 * silent drops (e.g. `body` written at the top level instead of under `fields`).
 */
declare function parseContentItem(raw: unknown): ContentItem;

/**
 * The content model (DESIGN §5). Content JSON is *pure data*: a value's type
 * comes from the template, never from the file, so these types model only the
 * JSON shape, not field semantics.
 */
/** A JSON-serialisable field value: a primitive, an array of primitives, or an
 *  array of flat records (a structured field such as authors). */
type FieldPrimitive = string | number | boolean | null;
/**
 * One structured entry, a flat record whose values are primitives, e.g. a single
 * author byline `{ name, user? }`. Kept flat so the value stays pure JSON data
 * (DESIGN §5): semantics still come from the template, not the file.
 */
type FieldRecord = Readonly<Record<string, FieldPrimitive>>;
type FieldValue = FieldPrimitive | readonly FieldPrimitive[] | readonly FieldRecord[];
/**
 * One content item: one file per item, filename = slug (DESIGN §5.4). Reserved
 * system keys sit at the top level; author values live in `fields`, isolated so
 * arbitrary token names can never collide with system keys. `template` is
 * optional here because a folder can supply the default (DESIGN §5.5); the
 * effective template is resolved during loading.
 */
interface ContentItem {
    readonly template?: string;
    readonly title: string;
    /** Whether the item goes to the live site (DESIGN §5.4). Absent in JSON means false. */
    readonly isPublished: boolean;
    readonly created?: string;
    /** Publish date, author-set; also the default loop sort key. ISO 8601. */
    readonly published?: string;
    readonly updated?: string;
    readonly fields: Readonly<Record<string, FieldValue>>;
}
/** A directory marked as a collection (DESIGN §5.6). */
interface CollectionConfig {
    /** A recursive collection includes every item beneath it. */
    readonly recursive: boolean;
}
/**
 * The reserved per-directory sort file (DESIGN §5.2): display name, ordered
 * contents, collection setting, and default template, all in one place.
 */
interface SortFile {
    readonly name?: string;
    readonly collection?: CollectionConfig;
    readonly defaultTemplate?: string;
    /** Ordered child filenames/dirnames; missing entries are tolerated. */
    readonly order: readonly string[];
}
/** A content page in the tree (DESIGN §5.1). */
interface ItemNode {
    readonly kind: 'item';
    /** Filename without extension = URL segment (DESIGN §5.3). */
    readonly slug: string;
    /** Content path from the root, e.g. "blog/going-static". */
    readonly path: string;
    readonly item: ContentItem;
    /**
     * What the parser dropped while reading this item (DESIGN §5.4 tolerance). Optional
     * because not every ItemNode goes through the diagnostics-returning parser (compile
     * tests construct nodes directly). The validation gate uses this to surface
     * `content.unknown-top-level-key` warnings rather than letting `body`-outside-`fields`
     * silently ship an empty page.
     */
    readonly parseDiagnostics?: ContentParseDiagnostics;
}
/** A directory in the tree; the file/folder layout *is* the site structure. */
interface DirNode {
    readonly kind: 'dir';
    /** Directory name = URL segment ('' for the tree root). */
    readonly slug: string;
    /** Display name (sort file `name`, or the humanised slug). */
    readonly name: string;
    /** Content path from the root, e.g. "blog" (''for the root). */
    readonly path: string;
    readonly collection?: CollectionConfig;
    readonly defaultTemplate?: string;
    /** Children ordered per the sort file, unknown trailing entries appended. */
    readonly children: readonly TreeNode[];
}
type TreeNode = DirNode | ItemNode;

/**
 * Parse a directory's sort file (DESIGN §5.2). Deliberately tolerant: a stale or
 * malformed sort file never blocks a publish, it just falls back to defaults.
 */
declare function parseSortFile(raw: unknown): SortFile;

/**
 * The storage port an adopter implements (DESIGN §11c): a flat key-to-bytes store, an
 * S3 bucket, an Azure container, a local folder, or a `Map`. The engine and the editor
 * reach every byte through this. Enumeration is deliberately *not* here, the content
 * index (§11d) owns it, so a store never needs a `list` or a scan, which is the
 * operation blob/S3 cannot do cheaply. Keys are opaque, POSIX-style site-relative paths
 * (e.g. "content/blog/post.json"); the store treats them as literal map keys.
 */
interface BlobStore {
    /** The bytes stored at `key`, or `undefined` if nothing is stored there. */
    get(key: string): Promise<Uint8Array | undefined>;
    /** Create or overwrite `key` with `bytes`. */
    put(key: string, bytes: Uint8Array): Promise<void>;
    /** Remove `key`. Deleting an absent key is a no-op (idempotent). */
    delete(key: string): Promise<void>;
}
/**
 * In-memory {@link BlobStore} backed by a plain map, the test double the engine's
 * store and index tests run against (the LSP guarantee that real adapters are
 * interchangeable). It copies bytes in and out, so a caller can never mutate stored
 * data by holding on to a buffer.
 */
declare class InMemoryBlobStore implements BlobStore {
    private readonly blobs;
    constructor(initial?: Readonly<Record<string, Uint8Array | string>>);
    get(key: string): Promise<Uint8Array | undefined>;
    put(key: string, bytes: Uint8Array): Promise<void>;
    delete(key: string): Promise<void>;
}

/**
 * The content-source port (DESIGN §3). The engine reads everything, content
 * tree, templates, components, through this narrow interface and never touches a
 * filesystem or blob store directly (CLAUDE.md §2/§3). Paths are POSIX-style and
 * relative to the site root (e.g. "content/blog/post.json").
 */
type EntryKind = 'file' | 'dir';
interface DirEntry {
    readonly name: string;
    readonly kind: EntryKind;
}
interface ContentSource {
    /**
     * Immediate children of a directory, sorted by `name` ascending. The sort is part
     * of the port contract (CLAUDE §2: determinism) — every implementation must return
     * a stable order so downstream Map/Set/loop iteration over the result produces
     * byte-identical output across runs.
     */
    list(dir: string): Promise<readonly DirEntry[]>;
    /** Read a UTF-8 text file; rejects if it does not exist. */
    readText(path: string): Promise<string>;
    /** Read raw bytes (for images and other binary assets); rejects if missing. */
    readBytes(path: string): Promise<Uint8Array>;
    /** Whether a file or directory exists at the path. */
    exists(path: string): Promise<boolean>;
}
/**
 * An in-memory {@link ContentSource} backed by a flat path→text map. Directories
 * are inferred from path prefixes. This is the test double the engine's loader
 * tests run against, the LSP guarantee that adapters are interchangeable.
 */
declare class InMemoryContentSource implements ContentSource {
    private readonly files;
    constructor(files: Readonly<Record<string, string | Uint8Array>>);
    readText(path: string): Promise<string>;
    readBytes(path: string): Promise<Uint8Array>;
    exists(path: string): Promise<boolean>;
    list(dir: string): Promise<readonly DirEntry[]>;
}

/**
 * The authors byline (DESIGN §6.11). An `authors` field stores an ordered list of
 * author refs; the compiler renders them as one grammatical line. Everything here
 * is pure: resolving a stored userName to its *current* display name needs the user
 * store, which the engine must not touch, so the host injects that lookup as an
 * {@link AuthorDirectory}. Without one, rendering falls back to the stored name
 * snapshot, so the engine always produces a byline on its own.
 */
/**
 * One stored author. A guest is just `{ name }`. A picked user also carries the
 * (immutable) `user` handle, so a later display-name change propagates to every
 * past byline and a bio link can be derived without a content migration. `name` is
 * the display-name snapshot, kept as the offline fallback (deleted user / no
 * directory). `href` is reserved for a future explicit guest link, additive.
 */
interface AuthorRef {
    readonly name: string;
    readonly user?: string;
    readonly href?: string;
}
/** Current display info for a stored `user` handle (host-injected; keeps the engine pure). */
interface AuthorEntry {
    readonly displayName: string;
    /** Reserved for a future bio-page link; unused by the current renderer. */
    readonly href?: string;
}
/** Resolve a stored `user` handle to its live display info, or undefined if unknown. */
type AuthorDirectory = (user: string) => AuthorEntry | undefined;
/**
 * A user offered to the byline picker (DESIGN §6.11): their current display name plus
 * the stable `user` handle the editor stores. The directory endpoint returns these; the
 * editor stores a picked one as `{ name, user }` (an {@link AuthorRef}).
 */
interface AuthorOption {
    readonly name: string;
    readonly user: string;
}
/**
 * Coerce a stored field value into author refs, dropping anything without a usable
 * name (a blank name, a non-object entry, a non-array value). The content boundary
 * already guarantees a flat record shape; this is the semantic narrowing the byline
 * needs.
 */
declare function toAuthorRefs(value: FieldValue | undefined): AuthorRef[];
/**
 * Join names into a grammatical byline (DESIGN §6.11): `0 → ""`, `1 → "A"`,
 * `2 → "A and B"`, `3+ → "A, B and C"` (no Oxford comma). Operates on the strings
 * as given; escaping is {@link renderAuthors}' job, so this stays a pure string join
 * the table tests can pin exhaustively.
 */
declare function joinAuthors(names: readonly string[]): string;
/**
 * Render an authors field value to an HTML byline. Each author's display name is
 * resolved live through the `directory` (falling back to the stored `name`
 * snapshot), HTML-escaped, then joined grammatically. The result is HTML, not plain
 * text, so future `<a>` bio links slot in without changing the compiler's path.
 */
declare function renderAuthors(value: FieldValue | undefined, directory?: AuthorDirectory): string;

interface CompileSiteOptions {
    /** Absolute base URL; enables sitemap.xml (and RSS when `rss` is set). */
    readonly baseUrl?: string;
    /** Emit an RSS feed for a collection (requires `baseUrl`). */
    readonly rss?: {
        readonly collection: string;
        readonly title: string;
        readonly description?: string;
    };
    /** Emit content.json, the lean content manifest (default true). */
    readonly contentIndex?: boolean;
    /** Image encoder for the media pipeline; without it, images keep their raw path. */
    readonly imageEncoder?: ImageEncoder;
    /** Max images encoded in parallel per page (default {@link DEFAULT_IMAGE_CONCURRENCY}). */
    readonly imageConcurrency?: number;
    /**
     * Resolve an author's stored `user` handle to its current display name (DESIGN
     * §6.11). Host-injected (the engine can't read the user store); without it,
     * `authors` bylines use the stored name snapshot.
     */
    readonly authorDirectory?: AuthorDirectory;
}
/** One compiled output file, text (HTML/XML/JSON) or binary (image variants, files). */
interface Artifact {
    /** Output path relative to the published root, e.g. "blog/first/index.html". */
    readonly path: string;
    readonly contents: string | Uint8Array;
}
/**
 * Compile a whole site to artifacts (DESIGN §15 step 9, compile phase). Walks the
 * content tree and renders every **published** item through its template, the
 * per-item template, else the nearest folder default (DESIGN §5.5). A site graph
 * resolves loops and references. Drafts are skipped; output is deterministic.
 */
declare function compileSite(source: ContentSource, options?: CompileSiteOptions): Promise<Artifact[]>;
/** Options for {@link compilePage}. */
interface CompilePageOptions {
    /**
     * Render this item instead of the one on disk, so the editor can preview live,
     * unsaved edits. Its `template` (or the folder default) selects the template.
     */
    readonly draft?: ContentItem;
    /**
     * A caller-supplied media lookup. The editor passes one that maps asset refs to
     * readable preview URLs (no variant generation), so a preview is instant; when
     * omitted, the real media phase runs (using `imageEncoder` if given).
     */
    readonly media?: MediaResolver;
    /** Image encoder for the real media phase; ignored when `media` is supplied. */
    readonly imageEncoder?: ImageEncoder;
    /** Author display-name resolver for `authors` bylines (DESIGN §6.11); see
     *  {@link CompileSiteOptions.authorDirectory}. */
    readonly authorDirectory?: AuthorDirectory;
}
/** The result of {@link compilePage}, the page HTML and any media it produced. */
interface CompiledPage {
    readonly html: string;
    /** Media artifacts from the real pipeline; empty when a `media` resolver was supplied. */
    readonly mediaArtifacts: readonly Artifact[];
}
/**
 * Compile a *single* item to HTML by its tree path, the editor's live preview
 * (DESIGN §6.7). It shares the whole compile path with {@link compileSite}: the
 * same tree, components, site graph (so loops/references resolve), template
 * resolution, and scope. Two differences serve the preview: an item is rendered
 * regardless of its `state` (you preview drafts), and the caller may inject a
 * `draft` override and a `media` resolver. Returns `undefined` when the path names
 * no item or the item has no resolvable template.
 */
declare function compilePage(source: ContentSource, itemPath: string, options?: CompilePageOptions): Promise<CompiledPage | undefined>;
/**
 * Build the resolution context the compiler consults for loops and references
 * (DESIGN §6.6) from a loaded content tree. Pure (no I/O): the host loads the tree
 * through a port, then hands it here. Exported so the editor's unit preview can
 * resolve loops/references against the real site, sharing this exact logic with the
 * publish path rather than re-implementing it.
 */
declare function buildResolveContext(tree: DirNode): ResolveContext;

/**
 * The media pipeline (DESIGN §8). Images are transformed into responsive,
 * modern-format variants and an `<img>` is expanded into an optimised `<picture>`.
 * The *policy* (which widths/formats, content-hashed names, picture markup) lives
 * here in the pure engine; the actual pixel encoding is a port (CLAUDE.md §3) so
 * the engine needs no native dependency and stays testable.
 */
type ImageFormat = 'avif' | 'webp' | 'jpeg' | 'png';
interface EncodeRequest {
    /** Candidate widths; the encoder must not upscale and should include the native width. */
    readonly widths: readonly number[];
    readonly formats: readonly ImageFormat[];
}
interface EncodedVariant {
    readonly format: ImageFormat;
    readonly width: number;
    readonly bytes: Uint8Array;
}
interface EncodedImage {
    readonly sourceWidth: number;
    readonly sourceHeight: number;
    readonly variants: readonly EncodedVariant[];
    /** Tiny inline placeholder (data URI) shown while the image loads. */
    readonly blurDataUri?: string;
}
/** The pixel-encoding port, implemented with sharp in `@nanoesis/adapter-sharp`. */
interface ImageEncoder {
    encode(input: Uint8Array, request: EncodeRequest): Promise<EncodedImage>;
}
/** One `<source>`'s worth of responsive variants. */
interface SourceSet {
    readonly format: ImageFormat;
    readonly mime: string;
    readonly srcset: string;
}
/** The compiled facts about one image, used to build its `<picture>`. */
interface ImageInfo {
    readonly width: number;
    readonly height: number;
    readonly sources: readonly SourceSet[];
    readonly fallbackSrc: string;
    readonly blurDataUri?: string;
}
/**
 * Per-item media lookup the compiler consults: a processed image by its asset
 * reference, or the published URL of a copied file. Built by the orchestrator.
 */
interface MediaResolver {
    image(assetRef: string): ImageInfo | undefined;
    file(assetRef: string): string | undefined;
}
/** A small, fast, non-cryptographic content hash (FNV-1a) for cache-busting names. */
declare function contentHash(bytes: Uint8Array): string;
/**
 * Encode one image into variants and compute its {@link ImageInfo} plus the
 * binary artifacts to publish. Variant filenames are content-hashed so they
 * cache-bust for free (DESIGN §8). `assetPath` is the published-relative path of
 * the original, e.g. "blog/assets/hero.jpg".
 */
declare function processImage(input: Uint8Array, assetPath: string, encoder: ImageEncoder): Promise<{
    readonly artifacts: readonly Artifact[];
    readonly info: ImageInfo;
}>;
/**
 * Build the `<picture>` markup for an image (DESIGN §8). `extraAttrs` carries the
 * resolved attributes from the original `<img>` (alt, class, …) onto the fallback.
 */
declare function buildPictureMarkup(info: ImageInfo, extraAttrs: ReadonlyArray<readonly [string, string]>): string;

/**
 * An item bound into loop scope (DESIGN §6.6). `{item}` resolves to its URL;
 * `{item.field}` to one of its fields.
 */
interface BoundItem {
    readonly url: string;
    readonly fields: Scope;
}
/** Loop ordering options parsed from a query loop's attributes. */
interface CollectionQuery {
    readonly sort?: string;
    readonly limit?: number;
}
/**
 * The resolution context (the site graph) the compiler consults for loops and
 * references (DESIGN §6.6, §10). Supplied by the host/orchestrator; kept behind
 * this interface so the compiler stays decoupled from the content tree.
 */
interface ResolveContext {
    /** Published items in a collection, for an automatic/query loop. */
    collection(path: string, query: CollectionQuery): readonly BoundItem[];
    /** Resolve a curated reference (content path) to a bound item if it exists & is published. */
    reference(path: string): BoundItem | undefined;
}
type ScopeValue = FieldValue | BoundItem;
/** A flat map of token name → value, for one scope (page fields, props, or a loop item). */
type Scope = Readonly<Record<string, ScopeValue>>;
/** Components keyed by their lowercased custom-element tag (DESIGN §6.8). */
type ComponentMap = ReadonlyMap<string, string>;
interface CompileInput {
    /** The entry template's HTML source. */
    readonly template: string;
    /**
     * The document shell (`templates/document.html`), if the site has one (DESIGN §6.10).
     * It wraps every page: compiled in the *same* page scope (so its `<head>` tokens are
     * page fields) with the page body filling its `<slot>`. Optional and additive, without
     * it, the page template is the whole document, exactly as before.
     */
    readonly document?: string;
    /** Resolved page-field values. */
    readonly scope: Scope;
    /** Available components, tag → source. */
    readonly components: ComponentMap;
    /** Site graph for loops/references; loops resolve to nothing without it. */
    readonly context?: ResolveContext;
    /** Per-item media lookup; without it, image/file tokens keep their raw value. */
    readonly media?: MediaResolver;
    /**
     * Resolves an author's stored `user` handle to its current display name (DESIGN
     * §6.11). Host-injected so the engine stays pure; without it, `authors` fields
     * fall back to the stored name snapshot.
     */
    readonly authorDirectory?: AuthorDirectory;
    /** The page template's co-located CSS, emitted once into `<head>` (DESIGN §6.9). */
    readonly templateStyle?: string;
    /** The page template's co-located JS, emitted once at the end of `<body>`. */
    readonly templateScript?: string;
    /** Component CSS by tag, injected as a scoped `<style>` at each instance's root. */
    readonly componentStyles?: ReadonlyMap<string, string>;
    /** Component JS by tag, emitted once per page (deduped across instances). */
    readonly componentScripts?: ReadonlyMap<string, string>;
}
/**
 * Compile a template + a content scope into HTML (DESIGN §6). Components, loops,
 * and media resolve recursively at compile time and substitution is per-context,
 * text, attribute, URL, rich-text, JSON-LD, which is what keeps output
 * injection-safe. Nothing of the framework survives into the output.
 */
declare function compileTemplate(input: CompileInput): string;

/**
 * The field-type registry (DESIGN §6.7). Field types and their rules are defined
 * *once* here and shared by the compiler, the editor's form generator, and (later)
 * Monaco's IntelliSense. This is the OCP extension point (CLAUDE.md §3): adding a
 * field type means adding an entry here, never editing a switch in the compiler or
 * the inference cascade.
 */
type FieldType = 'image' | 'file' | 'url' | 'email' | 'phone' | 'date' | 'time' | 'code' | 'richtext' | 'authors' | 'text' | 'shorttext';
/** How a resolved value is placed into output, the compiler reads this, never the type name. */
type ValueKind = 'text' | 'html' | 'url' | 'asset' | 'authors';
interface FieldTypeDef {
    readonly type: FieldType;
    readonly valueKind: ValueKind;
    /** Default editor control hint, consumed by the form generator (DESIGN §7). */
    readonly control: string;
    readonly multiline: boolean;
}
declare const FIELD_TYPES: Readonly<Record<FieldType, FieldTypeDef>>;
declare function isFieldType(value: string): value is FieldType;
declare function valueKindOf(type: FieldType): ValueKind;

/**
 * Derive the editor's form from a template, "the template is the schema"
 * (DESIGN §5.5/§6). Walking the same parse the compiler uses, each page-scope
 * token becomes one field whose control is inferred from where it sits, deduped by
 * name and labelled. This is the single source of truth the editor's form
 * generator and the compiler share (DESIGN §6.7).
 *
 * Inference is **component-aware**: when a component is used (`<doc file="{x}">`),
 * the bound field's type comes from how that prop is used *inside* the component,
 * not from the attribute's position on the usage. A component's prop types are
 * simply the field types of its own body, so the same derivation resolves them,
 * recursively, through nested components, with a cycle guard.
 */
interface DerivedField {
    readonly name: string;
    readonly type: FieldType;
    readonly label: string;
    readonly required: boolean;
    readonly multiline: boolean;
    readonly help?: string;
    /** Minimum character length (`data-minlength`); the gate warns when shorter. */
    readonly minLength?: number;
    /** Maximum character length (`data-maxlength`); the gate warns when longer. */
    readonly maxLength?: number;
    /** A curated multi-reference (a `data-each` loop field). */
    readonly multiple?: boolean;
    /** Collection the reference picker is scoped to (`data-pick-from`). */
    readonly pickFrom?: string;
}
/** Author-declared length bounds on a field value (`data-minlength`/`data-maxlength`). */
interface LengthConstraints {
    readonly minLength?: number;
    readonly maxLength?: number;
}
declare function deriveFields(template: string, components?: ComponentMap): DerivedField[];

/**
 * Stamping decisions (PROPOSAL-template-versioning §4.1, PROPOSAL-llm-guardrails §4).
 * `detectStamp` is pure — given the old and new template sources, it decides whether a
 * destructive change is taking place and reports the schema delta (added / removed
 * fields, plus per-token type changes).
 *
 * Two destructive shapes (PROPOSAL-llm-guardrails §1, both share the same recovery
 * story — auto-stamp a snapshot, flag for migration):
 *
 * 1. **Token-set asymmetric.** A token removed or renamed. The token *doesn't exist*
 *    in the new schema, so content authored against it has nowhere to render.
 * 2. **Token retained, type changed destructively.** The token *still exists*, but
 *    its inferred type can't losslessly render data authored under the old type
 *    (`richtext → shorttext` silently escapes HTML; `shorttext → image` reinterprets
 *    free text as a path). The §4 rule in {@link isDestructiveTypeChange} encodes
 *    which transitions are destructive vs widening.
 *
 * Both flow through `IndexedStore.write`'s auto-stamp path identically — the editor
 * UI and MCP both see the same recovery affordance, regardless of which destructive
 * shape triggered it.
 */
type TemplateKind = 'template' | 'component';
/** A field reference (name + inferred type) in the schema delta returned by `computeSchemaDelta`. */
interface SchemaFieldRef {
    readonly name: string;
    readonly type: FieldType;
}
/** A type change on a token retained across the edit (PROPOSAL-llm-guardrails §4-§5). */
interface TypeChange {
    readonly name: string;
    readonly from: FieldType;
    readonly to: FieldType;
    /** True when the change can't losslessly render data authored under `from`. */
    readonly destructive: boolean;
}
/**
 * Structured "what changed" report (PROPOSAL-llm-guardrails §5). Returned by
 * `IndexedStore.write` for template/component writes so callers (MCP, editor UI)
 * can react without re-deriving themselves. Empty arrays everywhere when an edit
 * changes only whitespace, comments, or non-token markup.
 */
interface SchemaDelta {
    readonly added: readonly SchemaFieldRef[];
    readonly removed: readonly SchemaFieldRef[];
    readonly typeChanged: readonly TypeChange[];
}
/** What `detectStamp` learned from comparing two template sources. */
interface StampDecision {
    readonly destructive: boolean;
    /** Tokens present in `oldSource` but absent in `newSource`, sorted for stability. */
    readonly removedTokens: readonly string[];
    /** Tokens present in `newSource` but absent in `oldSource`, sorted for stability. */
    readonly addedTokens: readonly string[];
    /** Tokens retained but whose inferred type changed (PROPOSAL-llm-guardrails §1). */
    readonly typeChanged: readonly TypeChange[];
}
/** The record `stampSnapshot` returns (and that the write path surfaces as `stamped`). */
interface StampRecord {
    /** The base template/component name (no `@v<N>` suffix). */
    readonly name: string;
    /** The integer version slot the snapshot occupies. */
    readonly version: number;
    /** The full path the snapshot was written to. */
    readonly snapshotPath: string;
}
/**
 * Whether a field's type change from `from` to `to` can't losslessly render content
 * authored against `from`. Drives both auto-stamp (destructive triggers a snapshot)
 * and the validation gate's `content.type-drift` warning per-item.
 *
 * The rule (PROPOSAL-llm-guardrails §4) reduces to:
 * - `anything → richtext` is safe (HTML escapes text fine).
 * - `richtext → anything else` is destructive (HTML becomes escaped text).
 * - Free-text → strict date/time is destructive (ISO required, free text won't parse).
 * - `date ↔ time` is destructive (both ISO but different shapes; values don't transfer).
 * - Same `valueKind` otherwise is non-destructive (text ↔ text, asset ↔ asset).
 * - Cross-`valueKind` into text is non-destructive (text representation works).
 * - Everything else cross-kind is destructive (text → asset/url/authors; asset ↔ url; …).
 */
declare function isDestructiveTypeChange(from: FieldType, to: FieldType): boolean;
/**
 * Compute the schema delta between two field lists. The richer counterpart to the
 * token-name diff in {@link detectStamp} — used by `IndexedStore.write` to surface
 * `schemaDelta` (PROPOSAL-llm-guardrails §5) without re-deriving on the caller side.
 * Pure; component-aware via `deriveFields`.
 */
declare function computeSchemaDelta(oldFields: readonly DerivedField[], newFields: readonly DerivedField[]): SchemaDelta;
/**
 * Decide whether the change from `oldSource` to `newSource` is destructive, and report
 * the schema diff (added / removed / typeChanged). Pure — no I/O. The field-name +
 * type set is derived via the same `deriveFields` the editor's form generator uses
 * (so the "schema" the decision is taken against is identical to what the form shows).
 * Component-aware: fields bound to component props are categorised by how that
 * component uses them internally, matching the compiler's resolution.
 */
declare function detectStamp(oldSource: string, newSource: string, components?: ComponentMap): StampDecision;

/**
 * The outcome of a {@link WorkingStore.rename}. Clobbering an existing destination, or
 * renaming a path that does not exist, are *expected* outcomes (a user renames to a name
 * already in use), so they are returned as data, not thrown (CLAUDE §2): the host maps
 * them to 409/404. The third case — refusing to rename into or out of the reserved
 * template-version namespace (`@v<N>` suffix, PROPOSAL §3) — is also returned as data,
 * not thrown, so the editor and MCP surface a tidy error to the user rather than a 500.
 */
type RenameResult = {
    readonly ok: true;
} | {
    readonly ok: false;
    readonly reason: 'exists' | 'missing' | 'reserved-version';
};
/**
 * Outcome of a {@link WorkingStore.write}. Returned as data so a caller that cares can
 * surface the stamp info to the user (the `/api/write` route attaches `stamped` to its
 * response; MCP's `write_file` passes it through), while a caller that does not care
 * can ignore the return value entirely (per JavaScript's discard semantics). For
 * non-template writes and for non-destructive template writes, `stamped` is absent.
 */
interface WriteResult {
    /** Snapshot record when a destructive template/component write triggered auto-stamp. */
    readonly stamped?: StampRecord;
    /**
     * True when the destructive write succeeded but the snapshot copy that should have
     * preserved the old bytes failed. The new current is in place (site stays
     * consistent); the snapshot's old bytes are not preserved. Surfaced as a Health
     * warning (`templates.stamp-incomplete`) so the migration-UX left-pane render
     * is known to be unavailable for items affected by this particular stamp.
     */
    readonly stampIncomplete?: boolean;
    /**
     * The schema diff between the old and new bytes for template/component writes
     * (PROPOSAL-llm-guardrails §5). Present whenever the target is a template or
     * component and there was a prior version; undefined for content writes, public
     * files, and brand-new templates. Carries `added`/`removed` (with types) and
     * `typeChanged` (with per-change `destructive` flag). MCP callers consume this to
     * react to schema impact without re-deriving; the editor surfaces a summary in the
     * stamp banner.
     */
    readonly schemaDelta?: SchemaDelta;
    /**
     * Parse diagnostics for content writes (smoke test 2026-05-29 silent-drop fix).
     * Present only for `content/*.json` writes whose JSON parsed successfully but
     * carried top-level keys the parser dropped (e.g. `body` written outside
     * `fields`). Undefined when the write is not a content item, parsing threw, or
     * everything was recognised. MCP `write_file` surfaces this immediately so an
     * LLM author sees the silent-drop the moment it happens rather than discovering
     * empty rendered pages later.
     */
    readonly parseDiagnostics?: ContentParseDiagnostics;
}
/**
 * The editor's working store (DESIGN §11c): a read {@link ContentSource} for
 * compile/validate, plus the write/delete/rename the editor content API needs. The
 * editor API depends on this port, never a concrete store, so the index-backed
 * {@link IndexedStore} (over an adopter's get/put/delete) and the blob adapter are
 * interchangeable behind it.
 */
interface WorkingStore extends ContentSource {
    /**
     * Create or overwrite `path` with raw bytes (binary-correct). Returns a
     * {@link WriteResult}; callers that do not care about stamp info can ignore it.
     * Destructive writes to `templates/*.html` / `components/*.html` auto-stamp a
     * snapshot of the prior bytes before the new bytes land (PROPOSAL §4.1).
     */
    write(path: string, contents: Uint8Array): Promise<WriteResult>;
    /** Delete a file, or a whole directory subtree; a missing path is a no-op. */
    delete(path: string): Promise<void>;
    /** Move a file or subtree; refuses to clobber, to rename a missing path, or to
     *  rename into or out of the reserved template-version namespace (`@v<N>`). */
    rename(from: string, to: string): Promise<RenameResult>;
    /**
     * Manually stamp a snapshot of the current `<kind>/<name>.html` to the next
     * `<name>@v<N+1>.html` slot (PROPOSAL §4.1 + §6, the manual "Stamp version" action).
     * Used for the additive-but-want-a-clean-break case the auto-stamp path does not
     * trigger. Throws if the current file does not exist or `name` is already
     * versioned. Distinguished from `write` so the store can bypass its own
     * reserved-path refusal — snapshots are immutable to user-initiated `write`s but
     * the engine's stamp pathway needs to create them.
     */
    stamp(name: string, kind: TemplateKind): Promise<StampRecord>;
}

/**
 * The content index (DESIGN §11d): the sorted set of every key in the working store,
 * the directory listing a flat blob store cannot give cheaply, lifted into one object
 * we own. Enumeration (the editor tree, compile) reads this, so the adopter's store
 * stays get/put/delete with no `list`. Persisted under a reserved key with a fixed-size
 * backup ring for integrity.
 */
interface ContentIndex {
    /** Monotonic, bumped on every save; orders the backup ring during recovery. */
    readonly version: number;
    /** Every key that exists, sorted and de-duplicated (deterministic, CLAUDE §2). */
    readonly keys: readonly string[];
    /** Checksum over version + keys, so a corrupt stored index is caught on read. */
    readonly checksum: string;
}
/** Reserved key prefix for the index and its backups; excluded from enumeration. */
declare const RESERVED_PREFIX = ".nanoesis/";
/** The empty index of a fresh store: version 0, no keys. */
declare function emptyIndex(): ContentIndex;
/**
 * Load the index, recovering through the backup ring if the live copy is missing or
 * corrupt (DESIGN §11d): the highest valid version among the ring slots wins. Returns
 * the empty index when nothing valid is found, a fresh store, or the accepted residual
 * case where the live index and every backup are lost (the files still exist by key,
 * but cannot be enumerated until something rewrites the index).
 */
declare function loadIndex(store: BlobStore): Promise<ContentIndex>;
/**
 * Write a new index whose keys are `nextKeys`, backing up the one it replaces first
 * (DESIGN §11d). The previous index goes into its ring slot (`version % ring`, oldest
 * overwritten), then the new index is written to the live key, a constant two writes
 * per save regardless of ring size. Returns the new index (the caller's next `prev`).
 */
declare function saveIndex(store: BlobStore, prev: ContentIndex, nextKeys: readonly string[]): Promise<ContentIndex>;
/** What a {@link reconcileIndex} did, for a maintenance UI/CLI and the startup signal. */
interface ReconcileResult {
    /** The index after reconciling (the prior one unchanged when nothing differed). */
    readonly index: ContentIndex;
    /** Keys now in the index that the prior index lacked (orphaned blobs, recovered). */
    readonly added: readonly string[];
    /** Keys the prior index held that no longer exist in the store (stale entries dropped). */
    readonly removed: readonly string[];
}
/**
 * Rebuild the index from the store's *actual* keys (DESIGN §11d): the safety net for a
 * store that got files by a path that bypassed the index, where the files are present
 * by key but unenumerable. The engine cannot list a {@link BlobStore} (none is required
 * to, §11c), so the caller supplies the real key set, an adapter that *can* enumerate
 * (e.g. `BlobContainer.list`) provides it; the rebuild stays pure index logic here.
 *
 * The reserved namespace is never indexed (the index is not its own content). When the
 * resulting key set matches the existing index this is a no-op, no write, no version
 * bump, so reconciling a healthy store is cheap and idempotent.
 */
declare function reconcileIndex(store: BlobStore, actualKeys: readonly string[]): Promise<ReconcileResult>;

/**
 * A {@link ContentSource} backed by the content index (DESIGN §11d) over a
 * {@link BlobStore}: enumeration (`list`/`exists`) is synthesised from the index's key
 * set, reads go straight to `get`. This is the working store the editor and compile run
 * against once an adopter provides only get/put/delete, every existing `list` caller is
 * unchanged because it still sees a `ContentSource`.
 *
 * Mutations (`write`/`delete`/`rename`) write the file(s) first, then the index
 * (content-first, §11d), keeping the in-memory copy in step so reads on this instance
 * reflect the change at once. The index is only rewritten when the key *set* changes, an
 * overwrite of an existing key is a single `put`.
 *
 * Construction does no I/O: the index loads (and recovers through the backup ring, §11d)
 * lazily on the first enumeration or mutation, so a host can wire the store synchronously.
 *
 * The prefix-synthesis below mirrors {@link InMemoryContentSource} exactly (a cross-check
 * test pins them together); the two are interchangeable implementations of the port.
 */
declare class IndexedStore implements WorkingStore {
    private readonly store;
    private index;
    private loading;
    /**
     * Per-instance mutex (DESIGN §11d): every mutation (`write`/`delete`/`rename`/`reconcile`)
     * runs through {@link serializeMutation}, which chains onto the previous mutation's
     * completion. Without it, two concurrent mutations both read the cached `this.index`,
     * each compute their "next" key set from that stale snapshot, and the second
     * `saveIndex` clobbers the first — the file writes land in blob, but the index keeps
     * stale references or loses fresh ones (the "I deleted it and it's still there" or
     * "I added it and it didn't appear" bugs surfaced dogfooding the marketing site,
     * 2026-05-28). Reads (`list`/`exists`/`readBytes`) are *not* serialised; an
     * in-flight mutation simply means a reader sees the pre-mutation index, eventually
     * consistent and safe. Crashes inside `work()` release the lock so a single failure
     * does not deadlock subsequent operations.
     */
    private mutationLock;
    constructor(store: BlobStore);
    /** The index, loaded once on first need and cached (mutations replace the cached copy). */
    private loaded;
    private serializeMutation;
    list(dir: string): Promise<readonly DirEntry[]>;
    readText(path: string): Promise<string>;
    readBytes(path: string): Promise<Uint8Array>;
    exists(path: string): Promise<boolean>;
    /**
     * Create or overwrite `key`. The index is rewritten only when `key` is new (an
     * overwrite leaves the key set unchanged, so editing an item is a single `put`).
     *
     * **Auto-stamp** (PROPOSAL §4.1): a destructive write to a template or component
     * path (one that removes a token from the existing source) snapshots the prior
     * bytes to `<base>@v<N+1>.html` before the new bytes land. Order is
     * **current first, snapshot second** (§16.3): if the snapshot copy fails after
     * the current write succeeded, the site stays consistent — only the migration
     * UX's left-pane comparison is unavailable for items affected by this stamp,
     * surfaced as `templates.stamp-incomplete`.
     *
     * Direct writes to a reserved-version path are refused (snapshots are
     * immutable; the user retires them via `delete`, not by overwriting them).
     */
    write(key: string, bytes: Uint8Array): Promise<WriteResult>;
    /**
     * Manually stamp a snapshot of the current `<kind>/<name>.html` to the next available
     * `<name>@v<N+1>.html` slot. Uses the same internal `this.store.put` pathway the
     * auto-stamp uses (so the public `write`'s reserved-version refusal doesn't apply),
     * and runs under the mutation mutex so a concurrent destructive auto-stamp can't
     * race with a manual stamp.
     */
    stamp(name: string, kind: TemplateKind): Promise<StampRecord>;
    /**
     * Delete a file, or a whole directory subtree (every key under `key/`). Idempotent: a
     * path the index does not know is still deleted from the store (clearing an orphan),
     * and deleting nothing is a no-op.
     */
    delete(key: string): Promise<void>;
    /**
     * Move/rename a file, or a whole directory subtree (every key under `from/` is remapped
     * under `to/`). Clobbering an existing destination, or renaming a missing path, are
     * returned as data (`ok: false`), not thrown (CLAUDE §2), the same contract the host's
     * `/api/rename` enforces. (Mutating the reserved namespace is a programmer error and
     * still throws.)
     */
    rename(from: string, to: string): Promise<RenameResult>;
    /**
     * Rebuild this store's index from the *actual* keys the underlying store holds (DESIGN
     * §11d), recovering files that arrived by a path that bypassed the index. A
     * {@link BlobStore} cannot enumerate itself, so the caller supplies the real key set
     * from an adapter that can (e.g. `BlobContainer.list`). Unlike calling
     * {@link reconcileIndex} on a fresh store, this also refreshes the cached in-memory
     * index, so this live instance sees the recovered files immediately.
     */
    reconcile(actualKeys: readonly string[]): Promise<ReconcileResult>;
}

/**
 * Redirect generation (DESIGN §9). A move or slug rename changes a page's URL;
 * internal `ref:` links auto-update, but external/bookmarked/indexed URLs would
 * 404. The editor records the old→new mapping *at the moment of the move* (the only
 * actor that knows a move happened, to this stateless compiler a move is
 * indistinguishable from a delete+create) into a single reserved
 * `content/_redirects.json`. Publish reads that as just another input and emits a
 * Cloudflare/Netlify-style `_redirects` artifact at the published root.
 *
 * This module is pure: parsing untrusted JSON into typed rules, and turning rules +
 * the set of currently-live URLs into the artifact. The file read lives in the loader.
 */

/** One redirect rule. `from`/`to` are URL paths; a `*` in `from` is a wildcard
 *  (folder move), with `:splat` in `to` standing for the matched tail. */
interface RedirectRule {
    readonly from: string;
    readonly to: string;
    /** HTTP status; defaults to 301 (permanent). */
    readonly status?: number;
}
/**
 * Parse the reserved `content/_redirects.json` (untrusted input) into typed rules.
 * Malformed entries are dropped and a non-array yields no rules, a garbage redirect
 * file never blocks a publish (mirrors the sort-file contract, CLAUDE.md §2).
 */
declare function parseRedirects(raw: unknown): RedirectRule[];
/**
 * Build the `_redirects` artifact from the recorded rules and the set of
 * currently-live page URLs. Exact rules collapse chains (A→B→C ⇒ A→C), drop a rule
 * whose source is now a live page or that resolves to itself (covers cycles), and
 * last-wins on a duplicate source. Wildcard rules (folder moves) pass through,
 * deduped. All rules are sorted before emit so output is deterministic (CLAUDE.md §2).
 * Returns `undefined` when nothing remains to emit (no file written).
 */
declare function buildRedirects(rules: readonly RedirectRule[], liveUrls: ReadonlySet<string>): Artifact | undefined;

declare const DEFAULT_DIRS: {
    readonly content: "content";
    readonly templates: "templates";
    readonly components: "components";
    /** Static passthrough, copied verbatim to the published root (DESIGN §8). The
     *  engine never reads it; named here so hosts/editor share one source of truth. */
    readonly public: "public";
};
declare function loadContentTree(source: ContentSource, contentDir?: string): Promise<DirNode>;
/**
 * Read the reserved redirect map (`content/_redirects.json`, DESIGN §9) into typed
 * rules, or `[]` when absent. A missing or garbage file never blocks a publish,
 * the rules are emitted as the `_redirects` artifact by {@link buildRedirects}.
 */
declare function loadRedirects(source: ContentSource, contentDir?: string): Promise<RedirectRule[]>;
/** Editor-set site configuration (the reserved `content/_site.json`). */
interface SiteConfig {
    /** Absolute site base URL; when set, publish emits sitemap.xml and absolute RSS links. */
    readonly baseUrl?: string;
}
/**
 * Read the reserved site-config file (`content/_site.json`), or `{}` when absent or
 * garbage, config never blocks a publish. The editor writes it from the admin Settings;
 * `compileSite` reads it so a base URL set in the editor switches sitemap.xml on without
 * any host/env change (an explicit host `baseUrl` option still overrides it).
 */
declare function loadSiteConfig(source: ContentSource, contentDir?: string): Promise<SiteConfig>;
/**
 * Build the component map (tag → source) from the components directory, recursing
 * into subfolders. Subfolders are purely organizational (DESIGN §7): a component's
 * tag is its **basename** (`components/marketing/hero.html` → `<hero>`), because a
 * component is referenced by an HTML tag, which is a flat namespace (no slashes).
 * Two files sharing a basename collide on one tag, last wins here; the validation
 * gate reports it (`component-tag-collision`) so a publish can't ship the wrong one.
 */
declare function loadComponents(source: ContentSource, componentsDir?: string): Promise<ComponentMap>;
/** Component CSS by tag (co-located `<name>.css` files); DESIGN §6.9. */
declare function loadComponentStyles(source: ContentSource, componentsDir?: string): Promise<ReadonlyMap<string, string>>;
/** Component JS by tag (co-located `<name>.js` files); DESIGN §6.9. */
declare function loadComponentScripts(source: ContentSource, componentsDir?: string): Promise<ReadonlyMap<string, string>>;
/** Read a named template's source. */
declare function loadTemplate(source: ContentSource, name: string, templatesDir?: string): Promise<string>;
/** The reserved document-shell template name, `templates/document.html` (DESIGN §6.10). */
declare const DOCUMENT_SHELL = "document";
/**
 * The document shell source if the site defines one, else undefined. The shell wraps every
 * page (its `<head>`/chrome), so it's never selected as an item's template; it's optional.
 */
declare function loadDocumentShell(source: ContentSource, templatesDir?: string): Promise<string | undefined>;

/**
 * Contextual token inference (DESIGN §6.2). A token's control type is inferred from
 * *where it sits*, via a precedence cascade:
 *
 *   annotation → element+attribute rule → attribute-name rule → element rule → default
 *
 * Inference only covers the *obvious* cases, a heading is short text, an `<img src>` is
 * an image, an `<a href>` is a link, everything else is plain text. The non-obvious
 * controls, **rich text, date, and time**, are deliberately *not* inferred (no "why is
 * this div rich text?" surprise); they're opt-in via `data-type` (§6.5), e.g.
 * `<div data-type="richtext">{body}</div>`, `<div data-type="date">{published}</div>`.
 */
interface TokenContext {
    /** Lowercased tag name of the element the token sits in (text) or on (attribute). */
    readonly tag: string;
    /** Attribute name if the token is in an attribute; undefined if it's text content. */
    readonly attribute?: string;
    /** The token is the entire attribute value / text content (not a fragment of it). */
    readonly wholeValue: boolean;
    /** A `data-type` annotation on the element, if present (the escape hatch, §6.5). */
    readonly annotation?: string;
    /** Literal attribute text before the token, lowercased, to spot `mailto:`/`tel:`. */
    readonly valuePrefix?: string;
    /** Whether the element carries a `download` attribute (`<a … download>`). */
    readonly download?: boolean;
}
/** Infer the control type for a single token occurrence. */
declare function inferControl(ctx: TokenContext): FieldType;

/**
 * Token parsing (DESIGN §6). A token is `{name}` or a dotted `{item.title}`.
 * Dotted-ness alone distinguishes an author field (`{title}`) from a value bound
 * to the current loop item (`{item.title}`), DESIGN §6.3, so no extra syntax
 * is needed.
 */
interface TokenRef {
    /** The raw match including braces, e.g. "{item.title}". */
    readonly raw: string;
    /** The first path segment, e.g. "item" (the page field name when not dotted). */
    readonly name: string;
    /** All dotted segments, e.g. ["item", "title"]. */
    readonly path: readonly string[];
    /** True when dotted, a loop-bound value, not a page field. */
    readonly dotted: boolean;
}
/** Find every token occurrence in a string, in source order. */
declare function findTokens(input: string): TokenRef[];
/** If the trimmed input is exactly one token, return it; otherwise null. */
declare function wholeValueToken(input: string): TokenRef | null;

/**
 * Versioned-name conventions for templates + components (PROPOSAL §3, §2.9). A snapshot
 * lives at the same path as its current sibling with an `@v<N>` suffix appended to the
 * base name, e.g. `templates/article.html` (current) and `templates/article@v1.html`
 * (snapshot). The suffix is reserved: the engine refuses to mutate paths into or out of
 * the form, so users can't accidentally collide with it.
 *
 * Canonical form is strict: `@v` followed by a positive base-10 integer with no leading
 * zeros, exactly one suffix per name. `@v01`, `@v`, `@v1@v2` are not versions, just
 * unusually-named bare templates — the helper returns false rather than try to repair.
 */
/** True when `name` parses as `<base>@v<N>` in canonical form (no nesting). */
declare function isVersionedTemplateName(name: string): boolean;
/** The base name without any `@v<N>` suffix (returns the input unchanged if unversioned). */
declare function baseTemplateName(name: string): string;
/** The numeric version, or `null` if `name` is not in canonical versioned form. */
declare function versionNumber(name: string): number | null;
/** Build the canonical snapshot name for `base` at `version`. Throws on invalid input. */
declare function snapshotName(base: string, version: number): string;
/**
 * The next version slot to allocate, given the names of every snapshot already in play
 * (typically the result of enumerating siblings of the current template). One higher than
 * the maximum existing version; 1 if no snapshots exist. Gaps are preserved (deleting a
 * snapshot does not reuse its number) — the user can see gaps in their tree but the
 * sequence remains monotonic, matching the proposal §16.5 decision.
 */
declare function nextVersionNumber(existingNames: readonly string[]): number;
/**
 * True when `path` matches the canonical snapshot form for a template or component (under
 * `templates/` or `components/`, ending in `.html`, base name has an `@v<N>` suffix).
 * Used by {@link IndexedStore} to refuse renames into or out of the reserved namespace.
 *
 * Matching paths in other partitions (content, public) are *not* reserved — users can
 * name content files however they like; the engine only owns the convention inside the
 * partitions whose tokens it interprets.
 */
declare function isReservedVersionedPath(path: string): boolean;

/**
 * Template-version migration helpers (PROPOSAL-template-versioning §4.3, §6).
 *
 * `pendingMigrations` enumerates content items whose JSON has fields that don't match
 * the schema of their bound template — orphan fields (data the template no longer
 * renders) or missing required fields (template wants but JSON is empty). Drift is
 * computed per-item against the item's *bound* template (current name or snapshot pin),
 * so pinned items are still flagged if the pin itself has drift.
 *
 * `bestFitSnapshot` picks the most recent snapshot of a base whose tokens are a
 * superset of an item's fields — used by the migration UX's left-pane render to show
 * "this is how the item rendered before."
 *
 * `applyMigration` rewrites an item JSON per a user-supplied resolution (drop / rename
 * / keep / fill). Idempotent in the sense that a no-op resolution leaves the JSON
 * byte-identical; non-resolved keys are preserved.
 */
interface PendingMigrationItem {
    /** Full content path (e.g. "content/blog/post.json"). */
    readonly path: string;
    /** The item's `template` field — e.g. "article" or "article@v1". */
    readonly boundTemplate: string;
    /** The base name (no `@v<N>` suffix). For pinned items this is the family. */
    readonly currentTemplate: string;
    /** Fields in JSON that the bound template doesn't render. */
    readonly orphanFields: readonly string[];
    /** Required fields the bound template wants but the JSON's `fields` is empty for. */
    readonly missingRequiredFields: readonly string[];
    /** Best-fit snapshot name (e.g. "article@v2") for the migration UX's left pane,
     *  or `null` when no snapshot's tokens cover the item's fields. */
    readonly bestFitSnapshot: string | null;
}
interface PendingMigrations {
    readonly items: readonly PendingMigrationItem[];
    /** Items grouped by `currentTemplate` for the workspace's list view. */
    readonly byTemplate: ReadonlyMap<string, readonly PendingMigrationItem[]>;
}
interface MigrationResolution {
    /** Orphan field names to remove from the item JSON. */
    readonly drop: readonly string[];
    /** Map of orphan field → current-template field to copy the value into. */
    readonly rename: Readonly<Record<string, string>>;
    /** Orphan field names to leave in the item JSON (advisory; gate stops warning). */
    readonly keep: readonly string[];
    /** Map of current-template field → value to fill (for missing required fields). */
    readonly fill: Readonly<Record<string, string>>;
}
declare function pendingMigrations(store: WorkingStore): Promise<PendingMigrations>;
declare function bestFitSnapshot(store: WorkingStore, base: string, itemFieldNames: readonly string[]): Promise<string | null>;
declare function applyMigration(store: WorkingStore, itemPath: string, resolution: MigrationResolution): Promise<void>;

/**
 * The authoring reference (DESIGN §14). A single, generated description of the whole
 * template surface, every field type, annotation, loop, and token, that powers three
 * faces at once: the editor's in-app Syntax panel, a downloadable `.md` an author can
 * hand to an LLM, and (later) the MCP server's "how to author" resource. Generated, not
 * hand-maintained, so it can never drift from the engine that's actually running: the
 * field-type section is built from {@link FIELD_TYPES}, and a test asserts it covers
 * every registered type. This is the CLAUDE.md §1 tie-breaker in action, one source of
 * truth removes the concept of "docs you keep in sync."
 */
/** One documented item, a field type, an annotation, a loop construct. */
interface ReferenceEntry {
    readonly name: string;
    readonly summary: string;
    /** A minimal, idiomatic template snippet (rendered as an HTML code block). */
    readonly example?: string;
    /** A short supplementary line (e.g. the site's own collection list). */
    readonly detail?: string;
}
interface ReferenceSection {
    readonly title: string;
    readonly intro: string;
    readonly entries: readonly ReferenceEntry[];
}
interface AuthoringReference {
    readonly title: string;
    readonly intro: string;
    readonly sections: readonly ReferenceSection[];
}
/** Optional, site-specific facts that make the reference concrete (DESIGN §14). */
interface ReferenceContext {
    /** Folder paths that can be looped over or picked from (e.g. `/blog`). */
    readonly collections?: readonly string[];
    /** Component tag names available in the site (e.g. `hero`, `card`). */
    readonly components?: readonly string[];
}
/**
 * Build the structured reference. Pure: with no context it describes the grammar; given a
 * site's {@link ReferenceContext} it also lists that site's real collections and components.
 */
declare function buildAuthoringReference(context?: ReferenceContext): AuthoringReference;
/**
 * Project the structured reference to GitHub-flavoured Markdown, the download an author
 * hands to an LLM, and the same content the engine and editor agree on.
 */
declare function renderReferenceMarkdown(reference: AuthoringReference): string;

/**
 * Aggregate artifacts (DESIGN §15 step 7): sitemap, RSS feed, and a lean content
 * manifest. All are pure functions of the published pages, so output stays
 * deterministic, dates come from the content, never the wall clock (CLAUDE.md §2).
 */
interface PageEntry {
    /** Content path, e.g. "blog/going-static". */
    readonly path: string;
    /** Filename slug (last segment). */
    readonly slug: string;
    /** Site-relative URL, e.g. "/blog/going-static/". */
    readonly url: string;
    readonly title: string;
    readonly published?: string;
    readonly updated?: string;
    readonly summary?: string;
}
interface RssOptions {
    readonly baseUrl: string;
    readonly title: string;
    readonly description: string;
    /** Collection content path the feed covers, e.g. "blog". */
    readonly collection: string;
}
/** Plain-text content of an HTML fragment (collapses whitespace), for search/RSS. */
declare function textContent(html: string): string;
/** sitemap.xml over every published page (DESIGN §9 canonical URLs). */
declare function buildSitemap(entries: readonly PageEntry[], baseUrl: string): Artifact;
/** RSS 2.0 feed for a collection, newest first. */
declare function buildRss(entries: readonly PageEntry[], options: RssOptions): Artifact;
/**
 * A lean, public content manifest the dev owns (DESIGN §14): a JSON array of every
 * published page as `{ url, title, published?, summary? }`, in tree order (deterministic).
 * Emitted at the site root as `content.json`; the dev builds search, Fuse or anything,
 * from it however they like, rather than the engine shipping a pre-baked full-text index.
 */
declare function buildContentIndex(entries: readonly PageEntry[]): Artifact;

/**
 * Static analysis of a template's structure, used by the validation gate, the
 * media phase, and (later) the editor. Extracts the signals downstream needs:
 * required fields, looped collections, curated references, and image/file fields.
 *
 * Component-aware (like `deriveFields`): a field bound to a component prop
 * (`<doc image="{x}">`) is categorised by how that prop is used inside the
 * component, so the media phase processes it correctly.
 */
interface TemplateAnalysis {
    readonly requiredFields: readonly string[];
    readonly queryLoopPaths: readonly string[];
    readonly curatedLoopFields: readonly string[];
    readonly imageFields: readonly string[];
    readonly fileFields: readonly string[];
    /** Fields injected as HTML (DESIGN §6.4), where authors embed inline media. */
    readonly richTextFields: readonly string[];
    /** Author-declared length bounds per field (`data-minlength`/`data-maxlength`). */
    readonly constraints: ReadonlyMap<string, LengthConstraints>;
    /** Internal `ref:` targets written into this template's own markup (DESIGN §5.7). */
    readonly references: readonly string[];
}
declare function analyzeTemplate(source: string, components?: ComponentMap): TemplateAnalysis;

/**
 * The validation gate (DESIGN §10). Its purpose is **referential integrity**, the
 * site never ships a broken link. Errors block a publish; warnings inform. Results
 * are returned as data (not thrown), so a host can present the clear list a user
 * needs (CLAUDE.md §2: "3 things would break: …").
 */
type Severity$1 = 'error' | 'warning';
interface Diagnostic$1 {
    readonly severity: Severity$1;
    readonly code: string;
    readonly message: string;
    /** Content path of the item/dir the diagnostic concerns, when applicable. */
    readonly path?: string;
}
interface ValidationResult {
    readonly diagnostics: readonly Diagnostic$1[];
    readonly errors: readonly Diagnostic$1[];
    readonly warnings: readonly Diagnostic$1[];
    /** True when there are no errors, i.e. a publish may proceed. */
    readonly ok: boolean;
}
declare function validateSite(source: ContentSource): Promise<ValidationResult>;

/**
 * The pre-build hook port (DESIGN §11/CLAUDE.md §2). Runs once inside the publish
 * pipeline, after the validation gate has passed and before compile, so a host can
 * invoke the site's own asset build (Tailwind, esbuild, etc.) and have its output
 * picked up by the engine's `public/` passthrough.
 *
 * The engine never reaches a shell, that lives in a host-side adapter (e.g.
 * `ShellPreBuildHook` in `@nanoesis/adapter-shell`). A failed hook throws; the
 * orchestrator propagates that exactly like a compile error (sink untouched, no
 * purge, the live site is left as it was).
 */
interface PreBuildHook {
    /** Run the hook once. Throw to abort the publish, the sink stays untouched. */
    run(): Promise<void>;
}

/**
 * The CDN-purge port (DESIGN §3, §11). After a publish writes the new site, the host
 * invalidates the CDN so readers see it. Phase 1 purges *everything* in one call
 * (DESIGN §11), no per-path batching, no manifest. Per-path purge is a Phase-2
 * concern (DESIGN §12).
 */
interface PurgeService {
    /** Invalidate the entire cache for the published site. */
    purgeAll(): Promise<void>;
}
/**
 * A {@link PurgeService} that does nothing, the right choice for a local build, a
 * CDN-less deploy, and the M4 local example (no Cloudflare account). Publishing still
 * "purges"; there is simply nothing to invalidate.
 */
declare const noopPurgeService: PurgeService;

/**
 * The artifact-sink port (DESIGN §3, §11, §11c). The publish pipeline emits every
 * compiled output, HTML pages, image variants, copied public files, through this single
 * `write`, so the engine never touches a filesystem or blob store directly (CLAUDE.md
 * §2/§3). Publish does not wipe: it emits the full set and returns the written paths
 * (DESIGN §11c), so clearing or pruning the target is the adopter's job, done by the host
 * around the publish, not a step the engine owns.
 *
 * Paths are POSIX-style and relative to the published root (e.g. "blog/first/index.html").
 */
interface ArtifactSink {
    /** Write one artifact, creating any intermediate structure the backing store needs. */
    write(path: string, contents: string | Uint8Array): Promise<void>;
}
/**
 * An in-memory {@link ArtifactSink} backed by a flat path→contents map. This is the
 * test double the publish-orchestration tests write into, the LSP guarantee that
 * sinks are interchangeable (the filesystem and blob sinks must behave identically).
 */
declare class InMemoryArtifactSink implements ArtifactSink {
    private readonly store;
    write(path: string, contents: string | Uint8Array): Promise<void>;
    /** The artifacts written so far, path→contents, for assertions in tests. */
    get written(): ReadonlyMap<string, string | Uint8Array>;
}

interface PublishOptions extends CompileSiteOptions {
    /** CDN purge run once after upload; defaults to a no-op (DESIGN §11). */
    readonly purge?: PurgeService;
    /** Passthrough static-file directory copied verbatim to the root (default "public"). */
    readonly publicDir?: string;
    /**
     * Optional host-supplied build hook (e.g. Tailwind). Runs once after the gate passes
     * and before compile, so its output lands in `public/` in time for the passthrough.
     * A throwing hook aborts the publish; the sink is left untouched.
     */
    readonly prebuild?: PreBuildHook;
    /** Max sink writes in flight at once (default {@link DEFAULT_WRITE_CONCURRENCY}). */
    readonly writeConcurrency?: number;
}
/**
 * What the publish actually did to *content*, as opposed to artifacts. Counts come from a
 * walk of the content tree (the same definition the compiler uses, `item.isPublished`).
 * Surfaced so a host/UI can explain a publish whose written-file count looks surprising,
 * the silent first-publish footgun: a published site with no published pages.
 */
interface PublishSummary {
    /** Content items with `isPublished: true` — the pages the publish included. */
    readonly published: number;
    /** Content items with `isPublished` false/absent — the pages the publish skipped. */
    readonly drafts: number;
    /**
     * A friendly one-liner when `published === 0`, so a UI can warn an operator who just
     * scaffolded a draft home and hit Publish that nothing went live. Absent when at least
     * one page is published.
     */
    readonly firstPublishWarning?: string;
}
interface PublishResult {
    /** True when the validation gate passed and the site was published. */
    readonly ok: boolean;
    /** The validation gate's findings (DESIGN §10), present whether or not it passed. */
    readonly validation: ValidationResult;
    /** Output paths written to the sink, sorted; empty when validation blocked the publish. */
    readonly written: readonly string[];
    /** Per-content counts (the human-readable side of `written`); present when the tree loaded. */
    readonly summary?: PublishSummary;
}
/**
 * Run the Phase-1 publish pipeline (DESIGN §11) through ports: **validate → compile →
 * write → purge**. The pipeline is host-agnostic, the same orchestration drives a local
 * filesystem host (e.g. `host-mcp`: fs sink, no-op purge) and the Azure Function host (blob
 * sink, Cloudflare purge). Publish only emits artifacts and returns the full written-path list, clearing
 * or pruning the target is the adopter's job (DESIGN §11c), not a step here.
 *
 * A failed validation gate is returned as data, not thrown (CLAUDE.md §2): nothing is
 * written, nothing is purged, and `ok` is false with the diagnostics attached. This is
 * the "3 things would break: …" contract.
 *
 * `public/` files are read through the same {@link ContentSource} and written verbatim
 * to the published root, after the compiled pages, so a `public/` file deliberately
 * shadows a compiled output of the same path (matching the local build).
 */
declare function publishSite(source: ContentSource, sink: ArtifactSink, options?: PublishOptions): Promise<PublishResult>;

/**
 * The identity port (DESIGN §11). The engine has *no auth logic*, auth is purely an
 * HTTP-layer concern that lives in the host, but the port type lives here alongside
 * the other port interfaces (`ContentSource`, `ArtifactSink`, `PurgeService`,
 * `ImageEncoder`, `PreBuildHook`). Adapters implement this port without depending on
 * any host package, exactly mirroring how the storage and CDN adapters depend on the
 * engine's port types rather than a specific host.
 *
 * Hosts re-export these symbols for ergonomics (so a BYO user imports the identity
 * surface from the host package they're already using). The engine itself never reads
 * a request, never checks a role, it compiles data to data.
 */
/**
 * The authorization roles (DESIGN §11). Roles are a **set**, a principal may hold
 * several. They map onto the editor's surfaces:
 *   - `author`, the Content workspace (content authoring).
 *   - `developer`, the Templates workspace (templates, components, public files).
 *   - `admin`, everything: implies `author` + `developer`, and additionally owns
 *                   user management and editor settings/branding.
 * A user can be author-only, developer-only, both, or admin (which subsumes both).
 */
type Role = 'author' | 'developer' | 'admin';
interface Principal {
    /** Stable identifier (opaque, e.g. a UUID for local-jwt, the gateway's id otherwise). */
    readonly userId: string;
    /** Human-readable handle for display (the account menu). Not a stable key, userId is. */
    readonly username: string;
    readonly roles: readonly Role[];
}
interface IdentityProvider {
    /** Resolve a request (via its header getter) to a principal, or null if anonymous. */
    authenticate(getHeader: (name: string) => string | undefined): Promise<Principal | null>;
}
/** Whether a principal holds a role; `admin` implies every role. */
declare function hasRole(principal: Principal | null, role: Role): boolean;
/** Whether a principal may use any editing surface (author, developer, or admin). */
declare function canEdit(principal: Principal | null): boolean;
/**
 * The HTTP-side credential-management surface (DESIGN §11). Providers that own
 * passwords/tokens (e.g. the local-JWT adapter) expose this; the host mounts each
 * method as a route. Trusted-header / external-IdP providers omit it, the gateway
 * does login. Like {@link IdentityProvider}, the engine declares the contract but has
 * no auth logic; the host wires it and the adapter implements it.
 */
interface AuthEndpoints {
    /** Verify credentials, issue a fresh token. On first-run, bootstraps an admin. */
    login(req: LoginRequest): Promise<AuthResult<LoginSuccess>>;
    /** Verify the current token, issue a fresh one with the same revocation counter. */
    refresh(token: string): Promise<AuthResult<RefreshSuccess>>;
    /** Invalidate every token issued so far for the caller; idempotent. */
    logout(token: string): Promise<AuthResult<{
        readonly ok: true;
    }>>;
    /** Status info for a login UI (e.g. is this the first-run-becomes-admin path?). */
    state(): Promise<{
        readonly firstRun: boolean;
    }>;
    /**
     * Verify the caller's current password and replace it. The host calls this after
     * authenticating the request and passes the principal's userId, the adapter trusts
     * that the caller is who they say they are. Bumps loginCount (so other sessions die)
     * and returns a fresh token for the caller to swap in.
     */
    changePassword(userId: string, req: ChangePasswordRequest): Promise<AuthResult<ChangePasswordSuccess>>;
    /**
     * Mint a long-lived **personal access token** for `userId`, e.g. for the MCP HTTP
     * transport, where a token lives in a client's config. Optional: providers that don't
     * own tokens omit it (the route 404s). Unlike a session token it is *independent of the
     * login session*, it survives logins/logouts so a paste-once token keeps working, and is
     * revoked only by {@link revokeTokens} (or deleting the user). It still grants the user's
     * current roles, so the editor's role gate applies unchanged.
     */
    createToken?(userId: string): Promise<AuthResult<CreateTokenSuccess>>;
    /** Revoke every personal access token for `userId`; idempotent. Optional, paired with
     *  {@link createToken}. Does not touch the user's login sessions. */
    revokeTokens?(userId: string): Promise<AuthResult<{
        readonly ok: true;
    }>>;
}
interface ChangePasswordRequest {
    readonly currentPassword: string;
    readonly newPassword: string;
}
interface ChangePasswordSuccess {
    /**
     * A fresh JWT. Changing the password bumped loginCount, so the caller's prior JWT
     * is now invalid, the client must replace it to continue making requests.
     */
    readonly token: string;
}
interface CreateTokenSuccess {
    /** The minted personal access token. Shown to the user once; treated like a password. */
    readonly token: string;
    /** Expiry as epoch milliseconds, so a UI can show "expires on …". */
    readonly expiresAt: number;
}
interface LoginRequest {
    readonly username: string;
    readonly password: string;
}
interface LoginSuccess {
    readonly token: string;
    readonly principal: Principal;
    /** True when this call created the first admin (first-run mode). */
    readonly firstRunBootstrap: boolean;
}
interface RefreshSuccess {
    readonly token: string;
    /** The same principal the token represents, saves the client a separate /me call. */
    readonly principal: Principal;
}
/**
 * Structured response shape, success carries `value`, failure carries an HTTP
 * `status` plus a short machine-readable `error`. Matches the rest of the host
 * (boundary failures are data, not exceptions).
 */
type AuthResult<TOk> = {
    readonly ok: true;
    readonly value: TOk;
} | {
    readonly ok: false;
    readonly status: number;
    readonly error: string;
};
/**
 * The admin-only user-management surface (DESIGN §11, Phase B). Separate from
 * {@link AuthEndpoints} per ISP: a provider that owns credentials (the local-JWT
 * adapter) may also offer user administration, but a host that only needs login
 * shouldn't depend on the management methods. Like the other auth ports the engine
 * only declares it, the host mounts each method as an **admin-gated** route and
 * passes the *caller's* userId so the adapter can enforce the self-edit guard;
 * trusted-header / external-IdP deploys omit it (users are managed upstream).
 *
 * Two invariants the adapter must hold (both tested independently): **never zero
 * admins** (a delete or demotion removing the last `admin` is rejected) and **no
 * self-edit of role/existence** (an admin can't change their own role or delete
 * their own account). Role changes, password resets, and deletes all revoke the
 * target's existing sessions (DESIGN §11 `loginCount`).
 */
interface UserAdminEndpoints {
    /** Every user, password hashes omitted, in a deterministic order. */
    listUsers(): Promise<AuthResult<readonly UserSummary[]>>;
    /** Create a user with an admin-set initial password. Rejects a duplicate username. */
    createUser(req: CreateUserRequest): Promise<AuthResult<UserSummary>>;
    /**
     * Change a user's roles. `callerId` is the requesting admin, for the self-edit guard.
     * A role change bumps the target's `loginCount` (their stale token, which carries the
     * old roles, stops working, they re-auth with the new roles).
     */
    updateUser(callerId: string, targetId: string, req: UpdateUserRequest): Promise<AuthResult<UserSummary>>;
    /** Set a new password for a user (admin recovery). Bumps `loginCount` (kills their sessions). */
    resetPassword(targetId: string, req: ResetPasswordRequest): Promise<AuthResult<{
        readonly ok: true;
    }>>;
    /** Remove a user. `callerId` is for the self-edit guard; also honours never-zero-admins. */
    deleteUser(callerId: string, targetId: string): Promise<AuthResult<{
        readonly ok: true;
    }>>;
}
/** A user as exposed to admin tooling, never includes the password hash. */
interface UserSummary {
    readonly id: string;
    readonly username: string;
    /**
     * Optional human-friendly name for bylines and the account menu. Absent means
     * "fall back to {@link username}", consumers resolve `displayName ?? username`.
     * The raw (possibly absent) value is carried here so an edit form shows what's
     * actually set rather than the defaulted-to username.
     */
    readonly displayName?: string;
    readonly roles: readonly Role[];
    /** Epoch ms; record creation time. */
    readonly createdAt: number;
    /** Epoch ms; last write time. */
    readonly updatedAt: number;
    /** Derived: a lockout is currently in effect (brute-force protection). */
    readonly locked: boolean;
}
interface CreateUserRequest {
    readonly username: string;
    readonly password: string;
    readonly roles: readonly Role[];
    /** Optional display name; trimmed, and blank is treated as unset. */
    readonly displayName?: string;
}
interface UpdateUserRequest {
    readonly roles?: readonly Role[];
    /**
     * Optional display name. `undefined` leaves it unchanged; an empty (or blank)
     * string clears it back to the username fallback. A displayName-only change is
     * cosmetic and does not revoke the user's sessions.
     */
    readonly displayName?: string;
}
interface ResetPasswordRequest {
    readonly newPassword: string;
}

/**
 * URL and output-path generation (DESIGN §9). Directory-style URLs are derived
 * from the tree and served via the portable index.html-in-a-folder pattern, so no
 * host rewrite rules are needed. A folder's `index` item is its section landing
 * page. Canonical URLs carry a trailing slash; the other form is redirected.
 */
/**
 * The output file path for an item's content path.
 *   "about"      → "about/index.html"
 *   "blog/first" → "blog/first/index.html"
 *   "blog/index" → "blog/index.html"  (section landing page)
 *   "index"      → "index.html"       (site root)
 */
declare function outputPathForItem(contentPath: string): string;
/**
 * The canonical URL (trailing slash) for an item's content path.
 *   "about"      → "/about/"
 *   "blog/first" → "/blog/first/"
 *   "blog/index" → "/blog/"
 *   "index"      → "/"
 */
declare function urlForItem(contentPath: string): string;

/**
 * Context-aware escaping (DESIGN §6.4). Token substitution is never naive
 * find/replace, each context escapes differently, which is what keeps generated
 * artifacts injection-safe.
 *
 * In the compiler, HTML-text and attribute escaping mostly come *for free* from
 * re-serialising the parse5 AST. These helpers cover the cases the serializer
 * can't: building markup by hand, JSON-LD raw-text (`<script>` content is not
 * HTML-escaped), and neutralising dangerous URL schemes.
 */
/** Escape a value for an HTML text node. */
declare function escapeHtmlText(value: string): string;
/** Escape a value for a double-quoted HTML attribute. */
declare function escapeHtmlAttribute(value: string): string;
/**
 * Escape a value for *inside* a JSON string literal (the quotes are already in
 * the template: `"name": "{title}"`). `She said "hi"` becomes `She said \"hi\"`.
 */
declare function escapeJsonStringContent(value: string): string;
/**
 * Neutralise dangerous URL schemes in a whole-value `href`/`src`. Control
 * characters and whitespace are stripped before sniffing the scheme, smuggling
 * them into `java\nscript:` is a classic bypass. Returns `#` for a blocked URL,
 * otherwise the original value unchanged.
 */
declare function sanitizeUrl(url: string): string;

/**
 * The diagnostics + self-heal feature (CLAUDE.md §2: expected failures are data, not
 * exceptions; we generalise the validation gate's shape from "is this content
 * publishable?" to "is this whole installation healthy?"). A diagnostic is a port-agnostic
 * probe that returns {@link Finding}s; a repair is an idempotent fix that runs through the
 * same ports. Both layers know nothing of any specific host or adapter: when an adapter
 * fails a round-trip probe, the surfacing detail is whatever that adapter put in its
 * thrown error's message. Adapter authors who want richer detail write richer errors,
 * not a new interface on the engine.
 */
type Severity = 'error' | 'warning' | 'info';
/**
 * What broad area of the system a finding concerns; UI groups by this. Deliberately
 * coarse and stable — adding adapter-specific categories ("kv", "cloudflare", …) would
 * couple the engine to specific cloud concepts, which is what we are avoiding.
 */
type Source = 'content' | 'index' | 'auth' | 'connectivity' | 'templates';
/**
 * Arguments a repair handle carries through to its `Repair.run`. Plain string map so the
 * shape serialises cleanly across the API boundary; repairs that need richer types parse
 * them inside `run`. Per-item context (an item path, a snapshot name) lives here when one
 * generic repair handles many findings (e.g. `templates.rebind-item-to-current` rebinds
 * whichever path it's given).
 */
type RepairArgs = Readonly<Record<string, string>>;
/**
 * One thing the diagnostics gate found wrong (or noteworthy). Shape mirrors the
 * validation gate's {@link Diagnostic} type so a future merger is one step.
 */
interface Finding {
    /** Stable across runs of the same diagnostic, so the UI can dedupe and target repairs. */
    readonly id: string;
    readonly source: Source;
    readonly severity: Severity;
    /** Short human-readable headline ("Home template is missing"). */
    readonly title: string;
    /** Full prose explaining what's wrong and where it was detected. */
    readonly detail: string;
    /** Present iff the engine can fix this in-place. Manual fixes go in `detail` text. */
    readonly repair?: {
        readonly id: string;
        readonly label: string;
        /** Per-finding context (e.g. an item path) the registered repair needs to run. */
        readonly args?: RepairArgs;
    };
}
/**
 * The ports a diagnostic is allowed to depend on. Just the working store today, the
 * port the editor's content API already binds (DESIGN §11c). Round-trip probes exercise
 * `write`/`list`/`exists`/`readBytes`/`delete`; content checks just read. Anything beyond
 * those (a `PurgeService`, a `BlobStore`, …) is added here as a new optional field, never
 * a host- or adapter-specific surface.
 */
interface DiagnoseDeps {
    readonly store: WorkingStore;
}
/**
 * One probe. `id` is stable and used to namespace the finding ids it emits. `run` returns
 * the {@link Finding}s it generated (empty array when there's nothing to report).
 */
interface Diagnostic {
    readonly id: string;
    run(deps: DiagnoseDeps): Promise<readonly Finding[]>;
}
/**
 * One in-place fix. Idempotent: calling `run` twice does not double-write. The host
 * re-runs the diagnostic set after a repair to refresh the UI, so repairs don't have to
 * return findings themselves. `args` carries per-finding context from the {@link Finding}
 * that surfaced the repair handle (e.g. the item path a "rebind to current" repair acts on).
 */
interface Repair {
    readonly id: string;
    run(deps: DiagnoseDeps, args: RepairArgs): Promise<void>;
}

/**
 * The diagnostic registry: an in-process collection of {@link Diagnostic}s and
 * {@link Repair}s the host's `/api/admin/diagnostics` route runs. A buggy diagnostic does
 * not take down the page — its thrown error is caught and surfaced as a finding with
 * `id` "<diagnostic-id>.crashed", so the user sees the failure exactly the way they see
 * every other finding. (CLAUDE.md §2 spirit: failures are data, not exceptions, even when
 * the failure is the probe itself.)
 *
 * Ids are unique per registry; registering a duplicate throws, since that is a
 * programmer error, not user-visible data.
 */
interface DiagnosticRegistry {
    /** Register a diagnostic. Throws if its id is already in the registry. */
    add(diagnostic: Diagnostic): void;
    /** Register a repair. Throws if its id is already in the registry. */
    addRepair(repair: Repair): void;
    /** Run every registered diagnostic in order; concatenate their findings. */
    runAll(deps: DiagnoseDeps): Promise<readonly Finding[]>;
    /** Run the named repair with optional per-finding args, then return a fresh `runAll`
     *  so the UI re-renders. Args default to `{}` when the repair needs none. */
    runRepair(id: string, deps: DiagnoseDeps, args?: RepairArgs): Promise<readonly Finding[]>;
}
declare function createDiagnosticRegistry(): DiagnosticRegistry;

declare const workingStoreRoundTripDiagnostic: Diagnostic;

export { type Artifact, type ArtifactSink, type AuthEndpoints, type AuthResult, type AuthorDirectory, type AuthorEntry, type AuthorOption, type AuthorRef, type AuthoringReference, type BlobStore, type BoundItem, type ChangePasswordRequest, type ChangePasswordSuccess, type CollectionConfig, type CollectionQuery, type CompileInput, type CompilePageOptions, type CompileSiteOptions, type CompiledPage, type ComponentMap, type ContentIndex, type ContentItem, ContentParseError, type ContentSource, type CreateTokenSuccess, type CreateUserRequest, DEFAULT_DIRS, DOCUMENT_SHELL, type DerivedField, type DiagnoseDeps, type Severity as DiagnoseSeverity, type Source as DiagnoseSource, type Diagnostic$1 as Diagnostic, type Diagnostic as DiagnosticCheck, type DiagnosticRegistry, type DirEntry, type DirNode, type EncodeRequest, type EncodedImage, type EncodedVariant, type EntryKind, FIELD_TYPES, type FieldPrimitive, type FieldRecord, type FieldType, type FieldTypeDef, type FieldValue, type Finding, type IdentityProvider, type ImageEncoder, type ImageFormat, type ImageInfo, InMemoryArtifactSink, InMemoryBlobStore, InMemoryContentSource, IndexedStore, type ItemNode, type LengthConstraints, type LoginRequest, type LoginSuccess, type MediaResolver, type MigrationResolution, type PageEntry, type PendingMigrationItem, type PendingMigrations, type PreBuildHook, type Principal, type PublishOptions, type PublishResult, type PublishSummary, type PurgeService, RESERVED_PREFIX, type ReconcileResult, type RedirectRule, type ReferenceContext, type ReferenceEntry, type ReferenceSection, type RefreshSuccess, type RenameResult, type Repair, type RepairArgs, type ResetPasswordRequest, type ResolveContext, type Role, type RssOptions, type SchemaDelta, type SchemaFieldRef, type Scope, type Severity$1 as Severity, type SiteConfig, type SortFile, type StampDecision, type StampRecord, type TemplateAnalysis, type TemplateKind, type TokenContext, type TokenRef, type TreeNode, type TypeChange, type UpdateUserRequest, type UserAdminEndpoints, type UserSummary, type ValidationResult, type ValueKind, type WorkingStore, analyzeTemplate, applyMigration, baseTemplateName, bestFitSnapshot, buildAuthoringReference, buildContentIndex, buildPictureMarkup, buildRedirects, buildResolveContext, buildRss, buildSitemap, canEdit, compilePage, compileSite, compileTemplate, computeSchemaDelta, contentHash, contentTypeFor, createDiagnosticRegistry, deriveFields, detectStamp, emptyIndex, escapeHtmlAttribute, escapeHtmlText, escapeJsonStringContent, findTokens, hasRole, humanize, inferControl, isDestructiveTypeChange, isFieldType, isReservedVersionedPath, isVersionedTemplateName, joinAuthors, loadComponentScripts, loadComponentStyles, loadComponents, loadContentTree, loadDocumentShell, loadIndex, loadRedirects, loadSiteConfig, loadTemplate, nextVersionNumber, noopPurgeService, outputPathForItem, parseContentItem, parseRedirects, parseSortFile, pendingMigrations, processImage, publishSite, reconcileIndex, renderAuthors, renderReferenceMarkdown, sanitizeUrl, saveIndex, slugify, snapshotName, textContent, toAuthorRefs, urlForItem, validateSite, valueKindOf, versionNumber, wholeValueToken, workingStoreRoundTripDiagnostic };
