/**
 * sanity/release-coverage.ts
 *
 * Pure partitioning of a content release into the populations that matter for
 * observability — kept free of any Sanity-client import so it can be unit
 * tested in isolation (mirrors the perspective-ref pure-core pattern).
 *
 * The perspective diff in {@link SanityDocFetcher.identifyAffectedDocs} only
 * compares the canonical docs reachable from in-scope tasks, so its
 * `ReleaseImpact` cannot, by construction, see a release edit to a doc no task
 * references. These helpers take the wider `sanity::partOfRelease` footprint
 * and report what the canonical-scoped diff leaves invisible.
 */
/**
 * Slugs in the full release footprint (`sanity::partOfRelease`) that are not
 * canonical docs of any in-scope task. Sorted and deduped for stable output.
 *
 * Note: the footprint is the set of articles with a version in the release —
 * i.e. added/modified docs. Docs *removed* by the release outside the
 * canonical set are not observable through the release perspective (they are
 * absent from it), so they are out of scope for this signal; the apply-loop
 * concern is unmeasured *edits*, which this captures.
 */
export function computeUncoveredDocs(releaseFootprintSlugs, canonicalSlugs) {
    const canonical = new Set(canonicalSlugs);
    const uncovered = new Set();
    for (const slug of releaseFootprintSlugs) {
        if (slug && !canonical.has(slug))
            uncovered.add(slug);
    }
    return [...uncovered].sort();
}
/**
 * Document types that are navigation/structure, not evaluable documentation
 * content — so a release edit to one must NOT be reported as an "uncovered"
 * (unmeasured) doc.
 *
 * `DOCS_IN_RELEASE_QUERY` is deliberately type-agnostic (auto-scope needs every
 * release member), so the footprint also contains structural docs. A release
 * that adds a documentation page also versions its `docsNavSection` (the
 * nav-tree entry that lists the page) — navigation structure with no content an
 * agent reads. Without this filter that nav section is reported as an unmeasured
 * doc on essentially every real pre-release run (D0084 follow-up).
 *
 * This is a **denylist** of structural types, not an allowlist of content types,
 * on purpose: documentation content types are open-ended (`article`,
 * `openApiReference`, `typesReference`, `glossaryEntry`, `guide`, …) and any can
 * be pinned by a type-agnostic `id` ref (W0195), so allowlisting would silently
 * stop surfacing a genuinely uncovered content doc. Missing a structural type
 * here only leaves mild residual noise; extend as new structural types surface.
 */
export const NON_EVALUABLE_RELEASE_DOC_TYPES = new Set([
    "docsNavSection",
    "docsNavigation",
    "docsSection",
    "redirect",
]);
/** Display label for a {@link ReleaseImpactDoc}: `slug` if present, else id. */
function impactDocLabel(doc) {
    return doc.slug ?? doc.documentId;
}
/**
 * Document-key (D0084) generalization of {@link computeUncoveredDocs}: the docs
 * in the full type-agnostic release footprint (`DOCS_IN_RELEASE_QUERY`) whose
 * base `documentId` is NOT in the in-scope set, so a non-article release edit
 * outside the evaluated corpus is surfaced rather than silently dropped (the
 * article-only slug footprint could not see it).
 *
 * Structural members ({@link NON_EVALUABLE_RELEASE_DOC_TYPES}, e.g. a
 * `docsNavSection` versioned alongside a new page) are filtered out rather than
 * surfaced as unmeasured docs — only documentation content a task could pin and
 * score is reportable.
 *
 * Deduped on the document key and sorted by display label (slug, else id) for
 * stable output. Each footprint entry's `documentId` is expected to already be
 * the base id (the fetcher normalizes via `baseDocumentId` before calling).
 */
export function computeUncoveredReleaseDocs(releaseFootprint, inScopeDocumentIds) {
    const inScope = new Set(inScopeDocumentIds);
    const uncovered = new Map();
    for (const doc of releaseFootprint) {
        const key = doc.documentId;
        if (!key || inScope.has(key) || uncovered.has(key))
            continue;
        // Structural members the release versions alongside content (e.g. a
        // `docsNavSection`) are not scoreable documentation content and must not be
        // reported as unmeasured docs. Content types (any other type, incl.
        // id-ref-pinnable ones like `glossaryEntry`) still surface.
        if (doc.documentType &&
            NON_EVALUABLE_RELEASE_DOC_TYPES.has(doc.documentType))
            continue;
        uncovered.set(key, doc);
    }
    return [...uncovered.values()].sort((a, b) => impactDocLabel(a).localeCompare(impactDocLabel(b)));
}
/**
 * Partition a release into the three observability populations.
 *
 * `coveredAffected` and `unaffected` are read straight from the
 * canonical-scoped {@link ReleaseImpact} (added+modified, and unchanged);
 * `uncovered` is the set difference of the full footprint against the
 * canonical set.
 */
export function computeReleaseCoverage(releaseImpact, releaseFootprintSlugs, canonicalSlugs) {
    return {
        coveredAffected: [...releaseImpact.added, ...releaseImpact.modified].sort(),
        uncovered: computeUncoveredDocs(releaseFootprintSlugs, canonicalSlugs),
        unaffected: [...releaseImpact.unchanged].sort(),
    };
}
