// Pure helpers for the Local Knowledge Connector persistent schema (Epic #189, Issue
// #265). Split out from `local-knowledge-schema.ts` to keep both files under the 400-LOC
// budget. No `node:sqlite`, no fs, no clock — these helpers only inspect or rewrite
// strings the runtime (#193) reads from disk or surfaces to diagnostics.
function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isNonEmptyString(value) {
    return typeof value === "string" && value.length > 0;
}
function isFinitePositiveInt(value) {
    return typeof value === "number" && Number.isInteger(value) && value > 0;
}
function isFiniteNonNegative(value) {
    return typeof value === "number" && Number.isFinite(value) && value >= 0;
}
const NON_EMPTY_STRING_FIELDS = [
    ["id", isNonEmptyString, "must be a non-empty string"],
    ["displayName", isNonEmptyString, "must be a non-empty string"],
    ["vectorMetric", isNonEmptyString, "must be a non-empty string"],
    ["embeddingModelProvider", isNonEmptyString, "must be a non-empty string"],
    ["embeddingModelId", isNonEmptyString, "must be a non-empty string"],
    ["lifecycleState", isNonEmptyString, "must be a non-empty string"],
    ["storageReference", isNonEmptyString, "must be a non-empty string"],
];
const ROW_FIELD_CHECKS = [
    ...NON_EMPTY_STRING_FIELDS,
    ["vectorDimensions", isFinitePositiveInt, "must be a positive integer"],
    ["createdAt", isFiniteNonNegative, "must be a finite non-negative number"],
    ["updatedAt", isFiniteNonNegative, "must be a finite non-negative number"],
];
export function validateCapsuleRowShape(input) {
    if (!isRecord(input)) {
        return { ok: false, errors: ["capsuleRow must be an object"] };
    }
    const errors = [];
    for (const [field, guard, errorSuffix] of ROW_FIELD_CHECKS) {
        if (!guard(input[field])) {
            errors.push(`capsuleRow.${field} ${errorSuffix}`);
        }
    }
    if (errors.length > 0)
        return { ok: false, errors };
    return { ok: true, value: input };
}
// ─── Diagnostic path redaction ───────────────────────────────────────────────────
// Used by parser-diagnostic and indexing-job error construction so raw filesystem paths
// cannot land in audit logs or UI surfaces. Pure: reads no environment, no fs, no clock.
// The HOME prefix is supplied by the caller (the runtime in #193 resolves it once at
// boot) so this helper stays deterministic and trivially testable.
const REDACTED_MAX_CHARS = 1024;
// Matches the ASCII control range; control chars in filenames are pathological and must
// not flow into UI strings unredacted. Disable the lint rule because the regex IS the
// gate.
// eslint-disable-next-line no-control-regex
const CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
function stripControls(value) {
    return value.replace(CONTROL_RE, "");
}
function normalizeSeparators(value) {
    return value.replace(/\\/g, "/");
}
function trimTrailingSlashes(value) {
    let end = value.length;
    while (end > 0 && value.charCodeAt(end - 1) === 47) {
        end -= 1;
    }
    return end === value.length ? value : value.slice(0, end);
}
function truncateAtNul(value) {
    const nulAt = value.indexOf("\0");
    return nulAt === -1 ? value : value.slice(0, nulAt);
}
function replaceHomePrefix(value, homePrefix) {
    // Normalize the homePrefix to forward-slash form first so the comparison succeeds whether
    // the caller supplies `/Users/foo`, `C:\\Users\\foo`, or any mix. Strip trailing slashes so
    // callers can pass either form. The empty-prefix gate short-circuits a caller-supplied "/".
    const normalizedPrefix = trimTrailingSlashes(normalizeSeparators(homePrefix));
    if (normalizedPrefix.length === 0)
        return value;
    if (value === normalizedPrefix)
        return "~";
    // Match the prefix followed by a separator; prevents `/Users/foobar` being misread as
    // `/Users/foo` + `bar`.
    if (value.startsWith(`${normalizedPrefix}/`)) {
        return `~${value.slice(normalizedPrefix.length)}`;
    }
    return value;
}
const WINDOWS_DRIVE_RE = /^[A-Za-z]:\/(.*)$/;
function replaceDrivePrefix(value) {
    const match = WINDOWS_DRIVE_RE.exec(value);
    if (match === null)
        return value;
    return `<drive>/${match[1] ?? ""}`;
}
// Public boundary helper. Order matters: separator normalisation runs BEFORE home-prefix
// rewriting so a Windows-style homePrefix (e.g. `C:\\Users\\foo`) compares cleanly against a
// Windows-style input. Drive-letter masking runs LAST so it only applies to inputs that did
// NOT match the user's home (otherwise a Windows home like `C:\\Users\\victim\\docs` would
// be rewritten to `<drive>/Users/victim/docs` and the home-prefix could never match — the
// #265 Copilot finding). Each step is idempotent so repeated calls return the same string.
export function redactPathInDiagnostic(rawPath, options = {}) {
    if (typeof rawPath !== "string")
        return "";
    const homePrefix = options.homePrefix ?? "";
    const afterNul = truncateAtNul(rawPath);
    const noControls = stripControls(afterNul);
    const normalized = normalizeSeparators(noControls);
    const homeRewritten = replaceHomePrefix(normalized, homePrefix);
    const driveRewritten = replaceDrivePrefix(homeRewritten);
    if (driveRewritten.length <= REDACTED_MAX_CHARS)
        return driveRewritten;
    return `${driveRewritten.slice(0, REDACTED_MAX_CHARS)}…`;
}
