// Public type contracts for the connected repository context surface (Epic #177, Issue #178).
// All string content carried by these types is already redacted upstream (keiko-security policy);
// nothing here performs redaction, IO, hashing, clock reads, or randomness. Pure data + pure
// validators only. The schemaVersion discriminant follows the same evolution rule as
// EVIDENCE_SCHEMA_VERSION (ADR-0010 D2): a breaking change introduces a NEW literal member
// rather than mutating "1". Leaf-package rule (ADR-0019 direction 1) means no
// `@oscharko-dev/keiko-*` imports may appear in this module.
// ─── Schema version ───────────────────────────────────────────────────────────
export const CONNECTED_CONTEXT_SCHEMA_VERSION = "1";
export const SELECTED_SCOPE_KINDS = [
    "workspace-root",
    "directory",
    "files",
];
export const EVIDENCE_ATOM_PROVENANCE_KINDS = [
    "lexical-search",
    "file-listing",
    "excerpt-read",
    "structural",
    "git-history",
    "model-rerank",
];
export const EVIDENCE_ATOM_REDACTION_STATES = [
    "redacted",
    "raw-internal",
];
export const DEFAULT_EXPLORATION_BUDGET = {
    searchCallsMax: 16,
    filesReadMax: 32,
    excerptBytesMax: 131_072,
    modelInputTokensMax: 32_000,
    modelOutputTokensMax: 4_096,
    elapsedMsMax: 30_000,
    rerankCallsMax: 0,
};
export const RETRIEVAL_QUERY_KINDS = [
    "natural-language",
    "exact-symbol",
    "file-pattern",
    "regex",
];
export const CANDIDATE_OMISSION_REASONS = [
    "outside-scope",
    "binary",
    "generated",
    "ignored",
    "size-exceeded",
    "near-duplicate",
    "low-relevance",
    "redacted-only",
    "budget-exhausted",
    "tool-unavailable",
];
export const CONNECTED_FILE_ROLES = [
    "read-only",
    "editable",
];
export const UNCERTAINTY_MARKER_KINDS = [
    "no-evidence",
    "stale-evidence",
    "scope-incomplete",
    "budget-clipped",
    "tool-unavailable",
    "low-confidence",
];
// Module-scope regex (avoid per-call allocation; safe — no backtracking risk).
const WINDOWS_DRIVE_RE = /^[A-Za-z]:/;
const WINDOWS_DRIVE_ABSOLUTE_RE = /^[A-Za-z]:[\\/]/;
const WINDOWS_DEVICE_PREFIX_RE = /^[\\/]{2}[?.][\\/]/;
const WINDOWS_UNC_PREFIX_RE = /^[\\/]{2}[^\\/?.]/;
const REMOTE_URL_PREFIX_RE = /^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//;
const TRAVERSAL_SEGMENT_RE = /(^|[\\/])(?:\.{1,2})(?:[\\/]|$)/;
// The schema discriminant comparisons below are statically true at the type level (the
// constant equals the literal field type), so we widen to `string` before comparing. This
// keeps the validator honest against runtime inputs that bypass the type system — e.g.,
// objects materialized from JSON.parse or cross-version manifests.
function schemaMismatch(actual, expected) {
    return actual !== expected;
}
function isFiniteNonNegativeInteger(value) {
    return Number.isInteger(value) && value >= 0;
}
function isFinitePositiveInteger(value) {
    return Number.isInteger(value) && value > 0;
}
function isNonEmptyTrimmed(value) {
    return typeof value === "string" && value.trim().length > 0;
}
function hasInvalidPathPrefix(path) {
    if (path.startsWith("/")) {
        return true;
    }
    if (path.startsWith("\\\\")) {
        return true;
    }
    return WINDOWS_DRIVE_RE.test(path);
}
function hasInvalidSegments(path) {
    const segments = path.split("/");
    for (const segment of segments) {
        if (segment.length === 0) {
            return true;
        }
        if (segment === "." || segment === "..") {
            return true;
        }
    }
    return false;
}
export function isValidScopePath(path, options) {
    if (!options.mustBeRelative) {
        return false;
    }
    if (path.length === 0) {
        return false;
    }
    if (path.includes("\0")) {
        return false;
    }
    // Backslashes are not valid path separators in POSIX workspace-relative paths; reject
    // them before segment analysis so Windows-style traversals cannot slip through.
    if (path.includes("\\")) {
        return false;
    }
    if (hasInvalidPathPrefix(path)) {
        return false;
    }
    return !hasInvalidSegments(path);
}
function isValidWorkspaceRootPath(path) {
    if (!isNonEmptyTrimmed(path)) {
        return false;
    }
    if (path.includes("\0")) {
        return false;
    }
    if (WINDOWS_DEVICE_PREFIX_RE.test(path) || WINDOWS_UNC_PREFIX_RE.test(path)) {
        return false;
    }
    if (!WINDOWS_DRIVE_ABSOLUTE_RE.test(path) && REMOTE_URL_PREFIX_RE.test(path)) {
        return false;
    }
    if (!path.startsWith("/") && !WINDOWS_DRIVE_ABSOLUTE_RE.test(path)) {
        return false;
    }
    return !TRAVERSAL_SEGMENT_RE.test(path);
}
export function isValidLineRange(range) {
    if (!Number.isInteger(range.startLine) || !Number.isInteger(range.endLine)) {
        return false;
    }
    if (range.startLine < 1) {
        return false;
    }
    return range.endLine >= range.startLine;
}
export function isWithinBudget(usage, budget) {
    const dims = [
        [usage.searchCalls, budget.searchCallsMax],
        [usage.filesRead, budget.filesReadMax],
        [usage.excerptBytes, budget.excerptBytesMax],
        [usage.modelInputTokens, budget.modelInputTokensMax],
        [usage.modelOutputTokens, budget.modelOutputTokensMax],
        [usage.elapsedMs, budget.elapsedMsMax],
        [usage.rerankCalls, budget.rerankCallsMax],
    ];
    for (const dim of dims) {
        const used = dim[0];
        const cap = dim[1];
        if (!Number.isFinite(used) || used < 0) {
            return false;
        }
        if (used > cap) {
            return false;
        }
    }
    return true;
}
function pushIf(reasons, condition, reason) {
    if (condition) {
        reasons.push(reason);
    }
}
function buildResult(reasons) {
    return reasons.length === 0 ? { ok: true } : { ok: false, reasons };
}
function validateScopeKindPaths(scope, reasons) {
    const paths = scope.relativePaths;
    if (scope.kind === "workspace-root" && paths.length !== 0) {
        reasons.push("scope.relativePaths must be empty for workspace-root");
    }
    if (scope.kind === "directory" && paths.length !== 1) {
        reasons.push("scope.relativePaths must have exactly one entry for directory");
    }
    if (scope.kind === "files" && paths.length < 1) {
        reasons.push("scope.relativePaths must be non-empty for files");
    }
    for (const candidate of paths) {
        if (!isValidScopePath(candidate, { mustBeRelative: true })) {
            reasons.push("scope.relativePaths contains an invalid path");
            return;
        }
    }
}
function isPathWithinSelectedScope(scope, candidatePath) {
    if (scope.kind === "workspace-root") {
        return true;
    }
    return scope.relativePaths.some((scopePath) => candidatePath === scopePath || candidatePath.startsWith(`${scopePath}/`));
}
export function validateSelectedScope(scope) {
    const reasons = [];
    pushIf(reasons, schemaMismatch(scope.schemaVersion, CONNECTED_CONTEXT_SCHEMA_VERSION), "scope.schemaVersion mismatch");
    pushIf(reasons, !isNonEmptyTrimmed(scope.scopeId), "scope.scopeId empty");
    pushIf(reasons, !isNonEmptyTrimmed(scope.workspaceRoot), "scope.workspaceRoot empty");
    if (isNonEmptyTrimmed(scope.workspaceRoot)) {
        pushIf(reasons, !isValidWorkspaceRootPath(scope.workspaceRoot), "scope.workspaceRoot invalid");
    }
    pushIf(reasons, !SELECTED_SCOPE_KINDS.includes(scope.kind), "scope.kind invalid");
    if (SELECTED_SCOPE_KINDS.includes(scope.kind)) {
        validateScopeKindPaths(scope, reasons);
    }
    pushIf(reasons, !isFiniteNonNegativeInteger(scope.connectedAtMs), "scope.connectedAtMs invalid");
    if (scope.conversationId !== undefined) {
        pushIf(reasons, !isNonEmptyTrimmed(scope.conversationId), "scope.conversationId empty");
    }
    return buildResult(reasons);
}
function validateLedgerRef(ref, reasons, prefix) {
    if (schemaMismatch(ref.evidenceSchemaVersion, "1")) {
        reasons.push(`${prefix}.ledgerRef.evidenceSchemaVersion mismatch`);
    }
    if (!isNonEmptyTrimmed(ref.runId)) {
        reasons.push(`${prefix}.ledgerRef.runId empty`);
    }
    if (ref.atomId?.trim().length === 0) {
        reasons.push(`${prefix}.ledgerRef.atomId empty`);
    }
}
function isScoreInUnitInterval(score) {
    return Number.isFinite(score) && score >= 0 && score <= 1;
}
export function validateEvidenceAtom(atom) {
    const reasons = [];
    pushIf(reasons, schemaMismatch(atom.schemaVersion, CONNECTED_CONTEXT_SCHEMA_VERSION), "atom.schemaVersion mismatch");
    pushIf(reasons, !isNonEmptyTrimmed(atom.stableId), "atom.stableId empty");
    pushIf(reasons, !isValidScopePath(atom.scopePath, { mustBeRelative: true }), "atom.scopePath invalid");
    if (atom.lineRange !== undefined) {
        pushIf(reasons, !isValidLineRange(atom.lineRange), "atom.lineRange invalid");
    }
    pushIf(reasons, !isScoreInUnitInterval(atom.score), "atom.score out of range");
    const provenance = atom.provenance;
    if (typeof provenance !== "object" || provenance === null) {
        reasons.push("atom.provenance missing");
    }
    else {
        pushIf(reasons, !EVIDENCE_ATOM_PROVENANCE_KINDS.includes(atom.provenance.kind), "atom.provenance.kind invalid");
        pushIf(reasons, !isNonEmptyTrimmed(atom.provenance.tool), "atom.provenance.tool empty");
        pushIf(reasons, !isNonEmptyTrimmed(atom.provenance.queryFingerprint), "atom.provenance.queryFingerprint empty");
    }
    pushIf(reasons, !EVIDENCE_ATOM_REDACTION_STATES.includes(atom.redactionState), "atom.redactionState invalid");
    pushIf(reasons, !isFiniteNonNegativeInteger(atom.emittedAtMs), "atom.emittedAtMs invalid");
    if (atom.ledgerRef !== undefined) {
        validateLedgerRef(atom.ledgerRef, reasons, "atom");
    }
    return buildResult(reasons);
}
export function validateRetrievalQuery(query) {
    const reasons = [];
    pushIf(reasons, !RETRIEVAL_QUERY_KINDS.includes(query.kind), "query.kind invalid");
    pushIf(reasons, query.text.length === 0, "query.text empty");
    pushIf(reasons, !isFinitePositiveInteger(query.maxResults), "query.maxResults invalid");
    pushIf(reasons, !isFiniteNonNegativeInteger(query.emittedAtMs), "query.emittedAtMs invalid");
    return buildResult(reasons);
}
// UTF-8 byte length via WHATWG TextEncoder (Web standard; available on globalThis in Node 22).
// Using this instead of Buffer keeps the contracts package free of Node-only APIs.
const TEXT_ENCODER = new TextEncoder();
function utf8ByteLength(value) {
    return TEXT_ENCODER.encode(value).length;
}
function validatePackFileEntry(entry, scope, selectedPaths, reasons) {
    if (selectedPaths.has(entry.scopePath)) {
        reasons.push("pack.files contains duplicate scopePath");
    }
    else {
        selectedPaths.add(entry.scopePath);
    }
    if (!CONNECTED_FILE_ROLES.includes(entry.role)) {
        reasons.push("pack.files entry has invalid role");
    }
    if (!isValidScopePath(entry.scopePath, { mustBeRelative: true })) {
        reasons.push("pack.files entry has invalid scopePath");
    }
    if (!isPathWithinSelectedScope(scope, entry.scopePath)) {
        reasons.push("pack.files entry falls outside selected scope");
    }
    if (!isNonEmptyTrimmed(entry.selectionReason)) {
        reasons.push("pack.files entry has empty selectionReason");
    }
}
function validatePackExcerpt(excerpt, entryScopePath, scope, reasons) {
    if (excerpt.atom.scopePath !== entryScopePath) {
        reasons.push("pack.files excerpt atom.scopePath does not match parent scopePath");
    }
    const atomResult = validateEvidenceAtom(excerpt.atom);
    if (!atomResult.ok) {
        for (const reason of atomResult.reasons) {
            reasons.push(`pack.files excerpt ${reason}`);
        }
    }
    if (!isPathWithinSelectedScope(scope, excerpt.atom.scopePath)) {
        reasons.push("pack.files excerpt atom.scopePath falls outside selected scope");
    }
    if (excerpt.atom.redactionState === "raw-internal") {
        reasons.push("pack.files excerpt atom.redactionState raw-internal");
    }
    if (excerpt.contentBytes !== utf8ByteLength(excerpt.content)) {
        reasons.push("pack.files excerpt contentBytes mismatch");
    }
    return excerpt.contentBytes;
}
function collectPackAtomIds(files) {
    const atomIds = new Set();
    for (const entry of files) {
        for (const excerpt of entry.excerpts) {
            atomIds.add(excerpt.atom.stableId);
        }
    }
    return atomIds;
}
function validatePackFiles(files, scope, reasons) {
    const selectedPaths = new Set();
    let actualExcerptBytes = 0;
    for (const entry of files) {
        validatePackFileEntry(entry, scope, selectedPaths, reasons);
        for (const excerpt of entry.excerpts) {
            actualExcerptBytes += validatePackExcerpt(excerpt, entry.scopePath, scope, reasons);
        }
    }
    return { actualExcerptBytes, selectedPaths };
}
function validatePackOmitted(entries, scope, reasons, selectedPaths) {
    const omittedPaths = new Set();
    for (const [i, entry] of entries.entries()) {
        if (selectedPaths.has(entry.scopePath)) {
            reasons.push("pack.omitted overlaps selected scopePath");
        }
        if (omittedPaths.has(entry.scopePath)) {
            reasons.push("pack.omitted contains duplicate scopePath");
        }
        else {
            omittedPaths.add(entry.scopePath);
        }
        if (!CANDIDATE_OMISSION_REASONS.includes(entry.reason)) {
            reasons.push("pack.omitted has invalid reason");
        }
        if (!isValidScopePath(entry.scopePath, { mustBeRelative: true })) {
            reasons.push(`omitted[${i.toString()}].scopePath invalid`);
        }
        if (!isPathWithinSelectedScope(scope, entry.scopePath)) {
            reasons.push("pack.omitted entry falls outside selected scope");
        }
        if (!isFiniteNonNegativeInteger(entry.omittedAtMs)) {
            reasons.push("pack.omitted has invalid omittedAtMs");
        }
    }
}
function validatePackUncertainty(entries, validAtomIds, reasons) {
    for (const [i, entry] of entries.entries()) {
        if (!UNCERTAINTY_MARKER_KINDS.includes(entry.kind)) {
            reasons.push("pack.uncertainty has invalid kind");
        }
        if (!isNonEmptyTrimmed(entry.claim)) {
            reasons.push(`uncertainty[${i.toString()}].claim empty`);
        }
        if (!isFiniteNonNegativeInteger(entry.emittedAtMs)) {
            reasons.push("pack.uncertainty has invalid emittedAtMs");
        }
        const impactedIds = new Set();
        for (const atomId of entry.impactedAtomIds) {
            if (!isNonEmptyTrimmed(atomId)) {
                reasons.push(`uncertainty[${i.toString()}].impactedAtomIds invalid`);
                continue;
            }
            if (impactedIds.has(atomId)) {
                reasons.push(`uncertainty[${i.toString()}].impactedAtomIds duplicate`);
                continue;
            }
            if (!validAtomIds.has(atomId)) {
                reasons.push(`uncertainty[${i.toString()}].impactedAtomIds unknown`);
                continue;
            }
            impactedIds.add(atomId);
        }
    }
}
function checkBudgetDimension(used, cap, dimension, reasons) {
    if (!isFiniteNonNegativeInteger(cap)) {
        reasons.push(`budget.${dimension}Max not a finite non-negative integer`);
        return;
    }
    if (!Number.isFinite(used) || used < 0) {
        reasons.push(`pack.usage.${dimension} invalid`);
        return;
    }
    if (used > cap) {
        reasons.push(`pack.usage.${dimension} exceeds budget`);
    }
}
function validatePackBudget(pack, actualExcerptBytes, reasons) {
    checkBudgetDimension(pack.usage.searchCalls, pack.budget.searchCallsMax, "searchCalls", reasons);
    checkBudgetDimension(pack.usage.filesRead, pack.budget.filesReadMax, "filesRead", reasons);
    checkBudgetDimension(pack.usage.excerptBytes, pack.budget.excerptBytesMax, "excerptBytes", reasons);
    checkBudgetDimension(pack.usage.modelInputTokens, pack.budget.modelInputTokensMax, "modelInputTokens", reasons);
    checkBudgetDimension(pack.usage.modelOutputTokens, pack.budget.modelOutputTokensMax, "modelOutputTokens", reasons);
    checkBudgetDimension(pack.usage.elapsedMs, pack.budget.elapsedMsMax, "elapsedMs", reasons);
    checkBudgetDimension(pack.usage.rerankCalls, pack.budget.rerankCallsMax, "rerankCalls", reasons);
    if (actualExcerptBytes > pack.usage.excerptBytes) {
        reasons.push("pack.files excerpts exceed pack.usage.excerptBytes");
    }
    if (actualExcerptBytes > pack.budget.excerptBytesMax) {
        reasons.push("pack.files excerpts exceed budget.excerptBytesMax");
    }
}
export function validateConnectedContextPack(pack) {
    const reasons = [];
    pushIf(reasons, schemaMismatch(pack.schemaVersion, CONNECTED_CONTEXT_SCHEMA_VERSION), "pack.schemaVersion mismatch");
    pushIf(reasons, !isNonEmptyTrimmed(pack.stableId), "pack.stableId empty");
    const scopeResult = validateSelectedScope(pack.scope);
    if (!scopeResult.ok) {
        for (const reason of scopeResult.reasons) {
            reasons.push(`pack.${reason}`);
        }
    }
    const queryResult = validateRetrievalQuery(pack.query);
    if (!queryResult.ok) {
        for (const reason of queryResult.reasons) {
            reasons.push(`pack.${reason}`);
        }
    }
    const fileSummary = validatePackFiles(pack.files, pack.scope, reasons);
    validatePackBudget(pack, fileSummary.actualExcerptBytes, reasons);
    validatePackOmitted(pack.omitted, pack.scope, reasons, fileSummary.selectedPaths);
    validatePackUncertainty(pack.uncertainty, collectPackAtomIds(pack.files), reasons);
    pushIf(reasons, !isFiniteNonNegativeInteger(pack.emittedAtMs), "pack.emittedAtMs invalid");
    if (pack.ledgerRef !== undefined) {
        validateLedgerRef(pack.ledgerRef, reasons, "pack");
    }
    return buildResult(reasons);
}
