"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpecificationReviewError = exports.SPECIFICATION_REVIEW_PROTOCOL_VERSION = void 0;
exports.reviewSpecificationSync = reviewSpecificationSync;
exports.computeSpecificationReviewId = computeSpecificationReviewId;
exports.serializeSpecificationReviewResultCanonical = serializeSpecificationReviewResultCanonical;
const node_crypto_1 = require("node:crypto");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const planning_document_utils_1 = require("../planning/planning-document-utils");
const paths_1 = require("../paths");
const spec_registry_1 = require("../spec-registry");
const path_resolution_1 = require("../spec-registry/path-resolution");
const specification_contract_1 = require("../specification-contract");
exports.SPECIFICATION_REVIEW_PROTOCOL_VERSION = 2;
class SpecificationReviewError extends Error {
    code;
    constructor(code, message) {
        super(message);
        this.name = "SpecificationReviewError";
        this.code = code;
    }
}
exports.SpecificationReviewError = SpecificationReviewError;
const NO_WRITE_BOUNDARY = "Specification review reads the registered canonical specification and registry only; it writes no files, persists no review or readiness evidence, and assigns no workflow or work identity.";
const PLACEHOLDER_PATTERN = /(?:\bTBD\b|\bTODO\b|\bplaceholder\b|pending refinement|{{\s*[a-z_][a-z0-9_]*\s*}})/i;
const NONE_PATTERN = /^(?:[-*]\s*)?(?:none|none currently|no open questions|n\/a)\.?$/i;
const CANONICAL_REQUIRED_SECTIONS = [
    { category: "purpose", code: "SPEC_REVIEW_REQUIRED_PURPOSE", headings: ["Purpose"], label: "purpose" },
    { category: "problem-statement", code: "SPEC_REVIEW_REQUIRED_PROBLEM_STATEMENT", headings: ["Problem Statement"], label: "problem statement" },
    { category: "desired-outcome", code: "SPEC_REVIEW_REQUIRED_DESIRED_OUTCOME", headings: ["Desired Outcome"], label: "desired outcome" },
    { category: "non-goals", code: "SPEC_REVIEW_REQUIRED_NON_GOALS", headings: ["Non-Goals"], label: "non-goal evidence" },
    { category: "scope", code: "SPEC_REVIEW_REQUIRED_SCOPE", headings: ["Scope"], label: "scope evidence" },
    { category: "constraints", code: "SPEC_REVIEW_REQUIRED_CONSTRAINTS", headings: ["Constraints"], label: "constraint evidence" },
    { category: "success-evidence", code: "SPEC_REVIEW_REQUIRED_SUCCESS_EVIDENCE", headings: ["Success Criteria"], label: "success evidence" }
];
const LEGACY_REQUIRED_SECTIONS = [
    { category: "problem", code: "SPEC_REVIEW_REQUIRED_PROBLEM", headings: ["Problem"], label: "problem statement" },
    { category: "goals", code: "SPEC_REVIEW_REQUIRED_GOALS", headings: ["Goals", "Goal"], label: "goal evidence" },
    { category: "non-goals", code: "SPEC_REVIEW_REQUIRED_NON_GOALS", headings: ["Non-Goals", "Non Goals", "Non-goals"], label: "non-goal evidence" },
    { category: "constraints", code: "SPEC_REVIEW_REQUIRED_CONSTRAINTS", headings: ["Constraints", "Constraint"], label: "constraint evidence" },
    { category: "risks", code: "SPEC_REVIEW_REQUIRED_RISKS", headings: ["Risks", "Risk"], label: "risk evidence" },
    { category: "success-evidence", code: "SPEC_REVIEW_REQUIRED_SUCCESS_EVIDENCE", headings: ["Success Criteria", "Success Evidence"], label: "success evidence" }
];
function reviewSpecificationSync(repositoryRoot, options) {
    const root = requireRepositoryRoot(repositoryRoot);
    const reviewedAt = requireReviewedAt(options.reviewedAt);
    const reviewedBy = requireNonEmpty(options.reviewedBy, "Reviewer identity");
    const requestedPath = parseReviewPath(options.specPath);
    const registryPath = (0, path_resolution_1.resolveSpecIndexAbsolutePath)(root);
    const registry = readRegistry(registryPath);
    const matchingEntries = registry.entries.filter((entry) => normalizePath(entry.spec_path) === requestedPath);
    if (matchingEntries.length === 0) {
        throw new SpecificationReviewError("spec-unregistered", `Specification '${requestedPath}' is not the canonical path of a registered specification.`);
    }
    if (matchingEntries.length > 1) {
        throw new SpecificationReviewError("registry-unreadable", `Specification '${requestedPath}' is ambiguously registered by ${matchingEntries.length} entries.`);
    }
    const entry = matchingEntries[0];
    const absoluteSpecPath = node_path_1.default.resolve(root, requestedPath);
    const specificationBytes = readSpecification(absoluteSpecPath, requestedPath);
    const markdown = specificationBytes.toString("utf8");
    const parsed = (0, planning_document_utils_1.parseMarkdownFrontMatter)(markdown);
    const sections = (0, planning_document_utils_1.parseMarkdownSections)(parsed.bodyText);
    const profile = (0, specification_contract_1.detectSpecificationProfile)(collectH2Headings(parsed.bodyText));
    const frontMatterSpecId = parsed.hasFrontMatter
        ? (0, planning_document_utils_1.readTopLevelField)(parsed.frontMatterLines, "spec_id")
        : undefined;
    const findings = [];
    findings.push(createFinding({
        category: "identity",
        code: "SPEC_REVIEW_IDENTITY",
        severity: frontMatterSpecId === entry.id ? "informational" : "error",
        status: frontMatterSpecId === entry.id ? "resolved" : "unresolved",
        message: frontMatterSpecId === entry.id
            ? `Specification front matter identity '${entry.id}' matches the registered identity.`
            : `Specification front matter spec_id '${frontMatterSpecId ?? "(missing)"}' does not match registered identity '${entry.id}'.`,
        path: requestedPath
    }));
    const registryConsistent = normalizePath(entry.spec_path) === requestedPath;
    findings.push(createFinding({
        category: "registry-consistency",
        code: "SPEC_REVIEW_REGISTRY_CONSISTENCY",
        severity: registryConsistent ? "informational" : "error",
        status: registryConsistent ? "resolved" : "unresolved",
        message: registryConsistent
            ? `Registry entry '${entry.id}' resolves to the selected canonical specification path.`
            : `Registry entry '${entry.id}' does not resolve to the selected canonical specification path.`,
        path: requestedPath
    }));
    findings.push(createFinding({
        category: "specification-profile",
        code: "SPEC_REVIEW_SPECIFICATION_PROFILE",
        severity: profile.recognized ? "informational" : "error",
        status: profile.recognized ? "resolved" : "unresolved",
        message: profile.recognized
            ? `Specification uses the recognized '${profile.id}' heading profile.`
            : "Specification H2 headings do not exactly match the canonical profile or a bounded Day Shift 0.2.0 legacy profile.",
        path: requestedPath
    }));
    const requiredSections = profile.id === "day-shift-0.2.0-legacy"
        ? LEGACY_REQUIRED_SECTIONS
        : CANONICAL_REQUIRED_SECTIONS;
    for (const requirement of requiredSections) {
        const content = findSection(sections, requirement.headings);
        const complete = hasSubstantiveContent(content);
        findings.push(createFinding({
            category: requirement.category,
            code: requirement.code,
            severity: complete ? "informational" : "error",
            status: complete ? "resolved" : "unresolved",
            message: complete
                ? `Specification includes substantive ${requirement.label}.`
                : `Specification is missing substantive ${requirement.label}.`,
            path: requestedPath
        }));
    }
    const placeholderMatches = collectPlaceholderEvidence(parsed.bodyText);
    findings.push(createFinding({
        category: "placeholder-residue",
        code: "SPEC_REVIEW_PLACEHOLDER_RESIDUE",
        severity: placeholderMatches.length === 0 ? "informational" : "warning",
        status: placeholderMatches.length === 0 ? "resolved" : "unresolved",
        message: placeholderMatches.length === 0
            ? "Specification contains no recognized placeholder residue."
            : `Specification contains placeholder residue (${placeholderMatches.join(", ")}).`,
        path: requestedPath
    }));
    const openQuestions = findSection(sections, ["Open Questions", "Unresolved Questions"]);
    const missingCanonicalOpenQuestions = profile.id !== "day-shift-0.2.0-legacy"
        && !hasSubstantiveContent(openQuestions);
    const hasUnresolvedQuestions = !missingCanonicalOpenQuestions
        && openQuestions !== undefined
        && openQuestions.trim() !== ""
        && !NONE_PATTERN.test(openQuestions.trim());
    findings.push(createFinding({
        category: "unresolved-questions",
        code: "SPEC_REVIEW_UNRESOLVED_QUESTIONS",
        severity: missingCanonicalOpenQuestions ? "error" : hasUnresolvedQuestions ? "warning" : "informational",
        status: missingCanonicalOpenQuestions || hasUnresolvedQuestions ? "unresolved" : "resolved",
        message: missingCanonicalOpenQuestions
            ? "Canonical specifications must include substantive open-question evidence, using an explicit none value when no questions remain."
            : hasUnresolvedQuestions
                ? "Specification records unresolved questions that should be settled or explicitly accepted before planning."
                : "Specification records no unresolved questions.",
        path: requestedPath
    }));
    const sourceTraceable = hasTraceableSource(entry);
    findings.push(createFinding({
        category: "source-traceability",
        code: "SPEC_REVIEW_SOURCE_TRACEABILITY",
        severity: sourceTraceable ? "informational" : "error",
        status: sourceTraceable ? "resolved" : "unresolved",
        message: sourceTraceable
            ? `Registry source '${entry.source.type}' retains a non-placeholder reference.`
            : "Registry source traceability is missing or placeholder-like.",
        path: requestedPath
    }));
    const prerequisiteUnresolved = findings.filter((finding) => finding.status === "unresolved");
    const stabilitySeverity = prerequisiteUnresolved.some((finding) => finding.severity === "error")
        ? "error"
        : prerequisiteUnresolved.length > 0
            ? "warning"
            : "informational";
    findings.push(createFinding({
        category: "planning-stability",
        code: "SPEC_REVIEW_PLANNING_STABILITY",
        severity: stabilitySeverity,
        status: stabilitySeverity === "informational" ? "resolved" : "unresolved",
        message: stabilitySeverity === "informational"
            ? "Specification evidence is stable enough to begin planning."
            : "Specification evidence should be refined before planning begins.",
        path: requestedPath
    }));
    const workflowRecommendation = recommendWorkflowLevel(markdown, sections, entry);
    findings.push(createFinding({
        category: "workflow-fit",
        code: "SPEC_REVIEW_WORKFLOW_FIT",
        severity: "informational",
        status: "resolved",
        message: `Advisory workflow fit is ${workflowRecommendation.level}; review does not assign workflow level.`,
        path: requestedPath
    }));
    const unresolvedFindings = findings.filter((finding) => finding.status === "unresolved");
    const waivedFindings = [];
    const counts = countFindings(findings, waivedFindings.length);
    const planningReady = unresolvedFindings.length === 0;
    const specRevision = sha256(specificationBytes);
    const identityEvidence = {
        spec_id: entry.id,
        spec_path: requestedPath,
        spec_revision: specRevision,
        specification_profile: profile.id,
        protocol_version: exports.SPECIFICATION_REVIEW_PROTOCOL_VERSION,
        reviewed_at: reviewedAt,
        reviewed_by: reviewedBy,
        findings,
        counts,
        planning_readiness_recommendation: {
            status: planningReady ? "ready" : "refine",
            reason: planningReady
                ? "All declared specification review categories are resolved."
                : `${unresolvedFindings.length} review finding${unresolvedFindings.length === 1 ? " remains" : "s remain"} unresolved.`
        },
        workflow_level_recommendation: workflowRecommendation,
        unresolved_findings: unresolvedFindings,
        waived_findings: waivedFindings,
        read_only: true,
        no_write_boundary: NO_WRITE_BOUNDARY
    };
    return {
        review_id: computeSpecificationReviewId(identityEvidence),
        ...identityEvidence
    };
}
/** Returns the stable Task 01 identity for one complete normalized review result. */
function computeSpecificationReviewId(review) {
    return `spec-review-${sha256Hex(Buffer.from(canonicalJson(review))).slice(0, 24)}`;
}
/** Serializes a complete review result for canonical repository persistence. */
function serializeSpecificationReviewResultCanonical(review) {
    return canonicalJson(review);
}
function createFinding(input) {
    return {
        ...input,
        id: input.code,
        level: input.severity,
        summary: input.message,
        detail: input.message,
        artifactPath: input.path,
        evidenceSource: "spec review",
        noWriteBoundary: NO_WRITE_BOUNDARY,
        evidence: {
            category: "command-specific",
            details: { reviewCategory: input.category, status: input.status }
        }
    };
}
function requireRepositoryRoot(repositoryRoot) {
    const normalized = requireNonEmpty(repositoryRoot, "Repository root");
    const stats = node_fs_1.default.statSync(normalized, { throwIfNoEntry: false });
    if (stats === undefined || !stats.isDirectory()) {
        throw new SpecificationReviewError("invalid-input", "Repository root must be an existing directory.");
    }
    return node_fs_1.default.realpathSync(normalized);
}
function requireReviewedAt(value) {
    const normalized = requireNonEmpty(value, "Review time");
    const timestamp = Date.parse(normalized);
    if (!Number.isFinite(timestamp)) {
        throw new SpecificationReviewError("invalid-input", "Review time must be a valid ISO-8601 timestamp.");
    }
    return new Date(timestamp).toISOString();
}
function requireNonEmpty(value, label) {
    if (typeof value !== "string" || value.trim() === "") {
        throw new SpecificationReviewError("invalid-input", `${label} must be a non-empty string.`);
    }
    return value.trim();
}
function parseReviewPath(value) {
    try {
        return (0, paths_1.parseRepositoryRelativePath)(requireNonEmpty(value, "Specification path")).normalizedPath;
    }
    catch (error) {
        if (error instanceof SpecificationReviewError)
            throw error;
        throw new SpecificationReviewError("invalid-input", error instanceof Error ? error.message : "Specification path is invalid.");
    }
}
function normalizePath(value) {
    try {
        return (0, paths_1.parseRepositoryRelativePath)(value).normalizedPath;
    }
    catch {
        return value.replace(/\\/g, "/");
    }
}
function readRegistry(registryPath) {
    try {
        return (0, spec_registry_1.parseSpecIndexDocumentText)(node_fs_1.default.readFileSync(registryPath, "utf8"));
    }
    catch (error) {
        throw new SpecificationReviewError("registry-unreadable", `Specification registry is unreadable: ${error instanceof Error ? error.message : "unknown error"}`);
    }
}
function readSpecification(absolutePath, displayPath) {
    try {
        const stats = node_fs_1.default.statSync(absolutePath);
        if (!stats.isFile()) {
            throw new Error("path is not a file");
        }
        return node_fs_1.default.readFileSync(absolutePath);
    }
    catch (error) {
        throw new SpecificationReviewError("spec-unreadable", `Specification '${displayPath}' is unreadable: ${error instanceof Error ? error.message : "unknown error"}`);
    }
}
function findSection(sections, headings) {
    for (const heading of headings) {
        const matching = [...sections.entries()].find(([candidate]) => candidate.toLowerCase() === heading.toLowerCase());
        if (matching !== undefined)
            return matching[1];
    }
    return undefined;
}
function collectH2Headings(markdown) {
    return markdown.split(/\r?\n/u).flatMap((line) => {
        const match = line.match(/^##\s+(.+)$/u);
        return match === null ? [] : [match[1].trim()];
    });
}
function hasSubstantiveContent(content) {
    if (content === undefined || content.trim() === "")
        return false;
    return !PLACEHOLDER_PATTERN.test(content);
}
function collectPlaceholderEvidence(markdown) {
    const evidence = new Set();
    const patterns = [
        ["TBD", /\bTBD\b/i],
        ["TODO", /\bTODO\b/i],
        ["placeholder", /\bplaceholder\b/i],
        ["pending refinement", /pending refinement/i],
        ["template token", /{{\s*[a-z_][a-z0-9_]*\s*}}/i]
    ];
    for (const [label, pattern] of patterns) {
        if (pattern.test(markdown))
            evidence.add(label);
    }
    return [...evidence];
}
function hasTraceableSource(entry) {
    return entry.source.type.trim() !== ""
        && entry.source.ref.trim() !== ""
        && !PLACEHOLDER_PATTERN.test(entry.source.ref);
}
function recommendWorkflowLevel(markdown, sections, entry) {
    const governedSignals = markdown.match(/\b(?:security|privacy|compliance|audit|regulated|governance)\b/gi) ?? [];
    if (governedSignals.length >= 2) {
        return {
            level: "governed",
            advisory: true,
            reasons: ["Multiple governance, security, privacy, compliance, or audit signals are present."]
        };
    }
    const substantiveSections = [...sections.values()].filter(hasSubstantiveContent).length;
    if (substantiveSections >= 6 || entry.source.additionalMetadata.source_references !== undefined) {
        return {
            level: "structured",
            advisory: true,
            reasons: ["The specification spans several substantive evidence sections or source references."]
        };
    }
    return {
        level: "basic",
        advisory: true,
        reasons: ["The specification appears bounded enough for a lightweight planning flow."]
    };
}
function countFindings(findings, waived) {
    return {
        total: findings.length,
        error: findings.filter((finding) => finding.severity === "error").length,
        warning: findings.filter((finding) => finding.severity === "warning").length,
        informational: findings.filter((finding) => finding.severity === "informational").length,
        unresolved: findings.filter((finding) => finding.status === "unresolved").length,
        waived
    };
}
function sha256(contents) {
    return `sha256:${sha256Hex(contents)}`;
}
function sha256Hex(contents) {
    return (0, node_crypto_1.createHash)("sha256").update(contents).digest("hex");
}
function canonicalJson(value) {
    if (Array.isArray(value))
        return `[${value.map(canonicalJson).join(",")}]`;
    if (value !== null && typeof value === "object") {
        return `{${Object.entries(value)
            .sort(([left], [right]) => left.localeCompare(right))
            .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
            .join(",")}}`;
    }
    return JSON.stringify(value);
}
