/**
 * Source- and resource-tagged locator family (D0063, generalizing D0062).
 *
 * A {@link DocumentRef} records *what* a document is (its id, slug, title); a
 * locator records *where it lives and how to re-fetch it*. D0062 introduced the
 * locator for documents; D0063 generalizes it on a SECOND orthogonal axis so the
 * same substrate can locate content *releases* without cloning the document
 * machinery.
 *
 * Two orthogonal taxonomies:
 * - the backend `kind` axis — *which content system* ({@link SourceKind}:
 *   `sanity-content-lake`; future `google-drive`);
 * - the `resource` axis — *what kind of thing inside that backend*
 *   ({@link ResourceKind}: `document`, `release`; future), nested UNDER `kind`.
 *
 * A release is therefore NOT a new `SourceKind` — it lives inside the
 * `sanity-content-lake` backend as a perspective, sharing the same
 * `{projectId, dataset}` anchor and swapping the identity axis from
 * `documentId`+`revision` to `releaseName`. The existing
 * {@link SanityContentLakeLocator} becomes the `resource: "document"` case with
 * the discriminant OPTIONAL (absent ⇒ document), so every already-persisted
 * immutable locator type-checks unchanged. {@link SourceLocator} stays frozen as
 * the document-arm alias — zero churn for its existing consumers (doc-fetcher
 * port, DocumentRef, projections).
 *
 * Full design: docs/design-docs/resource-locator.md.
 */
/**
 * Default Sanity project AILF reads documentation from.
 *
 * Promoted here so {@link resolveLocator} and both consumers (the eval source
 * resolver and the dashboard `sanity-config`) agree on one value instead of
 * duplicating the literal. Project `3do82whm` hosts the `next` docs dataset.
 */
export const DEFAULT_DOCS_PROJECT_ID = "3do82whm";
/** Default Sanity dataset AILF reads documentation from. */
export const DEFAULT_DOCS_DATASET = "next";
/**
 * The default docs-source anchor, shared by both `resolve*` synthesizers so
 * legacy document and release refs default to the same `{projectId, dataset}`.
 */
function defaultSanityAnchor() {
    return {
        kind: "sanity-content-lake",
        projectId: DEFAULT_DOCS_PROJECT_ID,
        dataset: DEFAULT_DOCS_DATASET,
    };
}
/**
 * Give EVERY ref — new or legacy — a locator.
 *
 * A ref carrying an explicit `locator` returns it verbatim. A legacy ref
 * (written before this feature, so without a locator — and an immutable report
 * can never be backfilled) resolves to a `sanity-content-lake` locator built
 * from the default docs source. Because there is exactly one docs source today,
 * the synthesized default is correct for 100% of historical reports; only new
 * reports from a second source need to carry an explicit locator. This is what
 * lets consumers treat all reports uniformly without backfilling history.
 */
export function resolveLocator(ref) {
    if (ref.locator) {
        return ref.locator;
    }
    return {
        ...defaultSanityAnchor(),
        documentId: ref.documentId,
        // Omit when absent rather than emitting an explicit `revision: undefined`,
        // so synthesized locators compare and persist cleanly.
        ...(ref.revision !== undefined ? { revision: ref.revision } : {}),
    };
}
/**
 * The symmetric twin of {@link resolveLocator} for releases. Shares the anchor
 * default; the synthesis body differs only because it reads a different identity
 * field (`releaseName` instead of `documentId`+`revision`) — irreducible, not
 * duplication. A ref carrying an explicit `locator` returns it verbatim.
 */
export function resolveReleaseLocator(ref) {
    if (ref.locator) {
        return ref.locator;
    }
    return {
        ...defaultSanityAnchor(),
        resource: "release",
        releaseName: ref.releaseName,
    };
}
/**
 * Turn a locator into Sanity client / App SDK read params, or `null` for a
 * backend that isn't Sanity — the caller handles non-Sanity origins explicitly.
 * The resource axis forks only the perspective derivation: a release reads under
 * its `releaseName`; a document reads under its `perspective` (defaulting to
 * `"published"` when omitted).
 */
export function toSanityClientConfig(loc) {
    if (loc.kind !== "sanity-content-lake") {
        return null;
    }
    const perspective = loc.resource === "release"
        ? loc.releaseName
        : (loc.perspective ?? "published");
    return { projectId: loc.projectId, dataset: loc.dataset, perspective };
}
/** Narrow a {@link ResourceLocator} to the release arm. */
export const isReleaseLocator = (loc) => loc.kind === "sanity-content-lake" && loc.resource === "release";
/** Narrow a {@link ResourceLocator} to the Sanity document arm (resource absent ⇒ document). */
export const isDocumentLocator = (loc) => loc.kind === "sanity-content-lake" && loc.resource !== "release";
/** Compose the `system.release` document id from a release name. */
export const buildReleaseDocumentId = (name) => `_.releases.${name}`;
/** Recover a release name from its `_.releases.<name>` document id. */
export const releaseNameFromDocumentId = (id) => id.replace(/^_\.releases\./, "");
/**
 * Compose the release-version document id an Agent Actions `transform` targets:
 * `versions.<releaseName>.<publishedId>`. The service treats a `versions.…` id
 * as `forcePublishedWrite` — it mutates only this version, never the published
 * document.
 */
export const buildVersionDocumentId = (releaseName, publishedId) => `versions.${releaseName}.${publishedId}`;
/**
 * Recover the base (published) document id from any versioned form — the
 * inverse companion of {@link buildVersionDocumentId}. Strips a single leading
 * `drafts.` or `versions.<release>.` prefix so the published, draft, and
 * release-version ids of one logical document all normalize to the same value,
 * which is what makes them join under {@link sameDocument} / the document key.
 * A base id may itself contain dots (`api.reference.v3`); only the leading
 * version prefix is removed (`<release>` is a dot-free release name).
 */
export const baseDocumentId = (id) => id.replace(/^(?:drafts\.|versions\.[^.]+\.)/, "");
/**
 * The cross-version document key: do two locators name the same *logical*
 * document, regardless of which version (published / draft / release) each was
 * read under? Compares the `{kind, projectId, dataset, base documentId}` subset
 * (D0084). `documentType`, `perspective`, and `revision` are deliberately
 * excluded — they distinguish versions or are denormalized handles, not
 * identity. The cross-version join key the release-diff / auto-scope / metrics
 * slices key on instead of a raw `_id` comparison.
 */
export const sameDocument = (a, b) => a.kind === b.kind &&
    a.projectId === b.projectId &&
    a.dataset === b.dataset &&
    baseDocumentId(a.documentId) === baseDocumentId(b.documentId);
/**
 * The version key: the same logical document ({@link sameDocument}) read under
 * the same perspective. An absent `perspective` defaults to `"published"`, so a
 * locator that omits it and one that states `"published"` are the same version.
 */
export const sameVersion = (a, b) => sameDocument(a, b) &&
    (a.perspective ?? "published") === (b.perspective ?? "published");
/**
 * A stable string form of the document key for use as a `Map`/`Set` key. Equal
 * iff the locators {@link sameDocument}-match, so every version of one logical
 * document collapses to a single key. `:` is a safe delimiter — it cannot
 * appear in a Sanity project id, dataset name, or document id.
 */
export const documentMatchKey = (loc) => `${loc.kind}:${loc.projectId}:${loc.dataset}:${baseDocumentId(loc.documentId)}`;
/**
 * Construct the shared Content Lake anchor from resolved source coordinates,
 * falling back to the default docs source. The single anchor-construction
 * source of truth shared by the document locator builder (eval's
 * `buildSanityLocator`) and {@link buildSanityReleaseLocator}. `organizationId`
 * is omitted (not emitted as `undefined`) when the source lacks it, so anchors
 * compare and persist cleanly.
 */
export function buildSanityAnchor(source) {
    return {
        kind: "sanity-content-lake",
        projectId: source?.projectId ?? DEFAULT_DOCS_PROJECT_ID,
        dataset: source?.dataset ?? DEFAULT_DOCS_DATASET,
        ...(source?.organizationId
            ? { organizationId: source.organizationId }
            : {}),
    };
}
/** Stamp a full release locator from resolved coordinates + a release name. */
export function buildSanityReleaseLocator(source, releaseName) {
    return { ...buildSanityAnchor(source), resource: "release", releaseName };
}
/**
 * True only for a perspective that names a content release (a release ID).
 * The reserved perspective names — `"published"`, `"raw"`, `"drafts"`, and the
 * deprecated `"previewDrafts"` alias for drafts — are dataset-wide views, not
 * curated releases, so release-scoped machinery (release refs, release
 * footprints) must not treat them as one. The input is trimmed before
 * comparing (env-sourced values can carry stray whitespace) but never
 * case-folded — release IDs are case-sensitive, so `"Drafts"` is a release ID.
 */
export function isContentReleasePerspective(name) {
    const perspective = name.trim();
    return (perspective !== "" &&
        perspective !== "published" &&
        perspective !== "raw" &&
        perspective !== "drafts" &&
        perspective !== "previewDrafts");
}
/**
 * Package a report's loose `provenance.source` siblings into a canonical
 * {@link ReleaseRef}. A read-time normalizer: the run-release coordinates
 * already persist on the report (`RunContext.source`), so the report→release
 * relationship needs no producer or persistence change. Returns `null` when
 * the source pins no perspective or a non-release one (per
 * {@link isContentReleasePerspective}).
 */
export function deriveRunReleaseRef(source) {
    const name = source.perspective;
    if (!name || !isContentReleasePerspective(name)) {
        return null;
    }
    return {
        releaseName: name,
        locator: buildSanityReleaseLocator(source, name),
    };
}
/**
 * Exhaustiveness guard. Every match over the {@link ResourceLocator} union ends
 * in `default: return assertNever(loc)`, so adding a future `resource`/`kind`
 * that some code site forgot to handle is a compile error, not a silent runtime
 * bug.
 */
export function assertNever(x) {
    throw new Error(`Unhandled variant: ${JSON.stringify(x)}`);
}
