import { createHash } from "node:crypto";
import {
  chmod,
  lstat,
  mkdir,
  open,
  readFile,
  realpath
} from "node:fs/promises";
import path from "node:path";
import { verifyRunrailCandidateAudit } from "./runrail-deepseek-advisory-core.mjs";
import { verifyRunrailHunkContinuity } from "./runrail-hunk-continuity-core.mjs";
import {
  evaluateRunrailReviewSession,
  findRunrailReviewSession,
  isRunrailReviewDispositionActive,
  listRunrailReviewDispositions,
  verifyRunrailAuditV4,
  verifyRunrailReviewCoverage,
  verifyRunrailReviewDisposition,
  verifyRunrailReviewSession
} from "./runrail-review-session-core.mjs";

export const RUNRAIL_PROOF_GRAPH = Object.freeze({
  schemaVersion: 1,
  artifactType: "runrail-proof-graph",
  profile: "runrail-proof-graph-v1",
  product: "Runrail Local v1",
  maximumReviews: 20,
  maximumNodes: 10_000,
  maximumEdges: 50_000
});

const SHA256_PATTERN = /^[a-f0-9]{64}$/;
const GRAPH_ID_PATTERN = /^RPG-[A-F0-9]{24}$/;
const NODE_ID_PATTERN = /^(?:REV|CON|RUL|EVD|FND|HNK|DSP)-[A-F0-9]{20}$/;
const EDGE_ID_PATTERN = /^EDG-[A-F0-9]{24}$/;
const nodeTypes = new Set([
  "revision",
  "contract",
  "rule",
  "evidence",
  "finding",
  "hunk",
  "disposition"
]);
const edgeTypes = new Set([
  "evaluated-under",
  "matched-rule",
  "requires-evidence",
  "reported-finding",
  "contains-hunk",
  "classifies-finding",
  "supersedes-disposition",
  "follows-review",
  "supersedes-revision",
  "continues-hunk",
  "recurs-from"
]);
const findingSeverities = new Set(["high", "medium", "info"]);
const hunkStatuses = new Set(["reviewed", "needs-revision", "unreviewed"]);
const dispositionKinds = new Set([
  "useful",
  "not-relevant",
  "accepted-exception"
]);
const forbiddenSerializedTerms = [
  "\"repositoryPath\":",
  "\"repoFingerprint\":",
  "\"baseRef\":",
  "\"candidateRef\":",
  "\"baseCommit\":",
  "\"candidateCommit\":",
  "\"changedFiles\":",
  "\"diffSha256\":",
  "\"sourceContent\":",
  "\"rawVerifierOutput\":",
  "\"providerKey\":",
  "\"owner\":",
  "\"reason\":"
];

export class RunrailProofGraphError extends Error {
  constructor(code, message) {
    super(message);
    this.name = "RunrailProofGraphError";
    this.code = code;
  }
}

function fail(code, message) {
  throw new RunrailProofGraphError(code, message);
}

function isRecord(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (isRecord(value)) {
    return Object.fromEntries(
      Object.keys(value)
        .sort()
        .map((key) => [key, stableValue(value[key])])
    );
  }
  return value;
}

export function stableRunrailProofGraphJson(value) {
  return `${JSON.stringify(stableValue(value), null, 2)}\n`;
}

function sha256(value) {
  return createHash("sha256").update(value).digest("hex");
}

export function computeRunrailProofGraphChecksum(value) {
  return sha256(stableRunrailProofGraphJson(value));
}

function exactKeys(value, expected, label) {
  if (!isRecord(value)) fail("proof-graph-invalid", `${label} must be an object.`);
  const actual = Object.keys(value).sort();
  const wanted = [...expected].sort();
  if (
    actual.length !== wanted.length ||
    actual.some((key, index) => key !== wanted[index])
  ) {
    fail("proof-graph-invalid", `${label} has an invalid field set.`);
  }
}

function expectInteger(value, label, minimum = 0, maximum = Number.MAX_SAFE_INTEGER) {
  if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
    fail("proof-graph-invalid", `${label} must be a bounded integer.`);
  }
}

function expectBoolean(value, label) {
  if (typeof value !== "boolean") {
    fail("proof-graph-invalid", `${label} must be a boolean.`);
  }
}

function canonicalTimestamp(value, label, { nullable = false } = {}) {
  if (nullable && value === null) return;
  if (
    typeof value !== "string" ||
    !Number.isFinite(Date.parse(value)) ||
    new Date(value).toISOString() !== value
  ) {
    fail("proof-graph-invalid", `${label} must be a canonical UTC timestamp.`);
  }
}

function compareText(left, right) {
  return left < right ? -1 : left > right ? 1 : 0;
}

function isInsideOrEqual(candidate, parent) {
  const relative = path.relative(parent, candidate);
  return (
    relative === "" ||
    (
      relative !== ".." &&
      !relative.startsWith(`..${path.sep}`) &&
      !path.isAbsolute(relative)
    )
  );
}

function graphNodeId(prefix, identity) {
  return `${prefix}-${sha256(identity).slice(0, 20).toUpperCase()}`;
}

function graphEdgeId(type, from, to) {
  return `EDG-${sha256(
    `runrail-proof-graph-edge-v1\0${type}\0${from}\0${to}`
  )
    .slice(0, 24)
    .toUpperCase()}`;
}

function addNode(nodeMap, node) {
  if (nodeMap.has(node.id)) {
    const existing = stableRunrailProofGraphJson(nodeMap.get(node.id));
    if (existing !== stableRunrailProofGraphJson(node)) {
      fail("proof-graph-node-conflict", "ProofGraph node identity collision detected.");
    }
    return nodeMap.get(node.id);
  }
  nodeMap.set(node.id, node);
  if (nodeMap.size > RUNRAIL_PROOF_GRAPH.maximumNodes) {
    fail("proof-graph-size-boundary", "ProofGraph exceeded its node boundary.");
  }
  return node;
}

function addEdge(edgeMap, type, from, to) {
  const id = graphEdgeId(type, from, to);
  const edge = { id, type, from, to };
  if (edgeMap.has(id)) {
    if (stableRunrailProofGraphJson(edgeMap.get(id)) !== stableRunrailProofGraphJson(edge)) {
      fail("proof-graph-edge-conflict", "ProofGraph edge identity collision detected.");
    }
    return;
  }
  edgeMap.set(id, edge);
  if (edgeMap.size > RUNRAIL_PROOF_GRAPH.maximumEdges) {
    fail("proof-graph-size-boundary", "ProofGraph exceeded its edge boundary.");
  }
}

function verifyReviewInput(review) {
  exactKeys(
    review,
    ["session", "audit", "coverage", "dispositions"],
    "ProofGraph review input"
  );
  verifyRunrailReviewSession(review.session);
  verifyRunrailAuditV4(review.audit);
  verifyRunrailCandidateAudit(review.audit);
  verifyRunrailReviewCoverage(review.coverage, review.audit);
  verifyRunrailHunkContinuity(review.coverage);
  if (
    review.session.evidence.auditChecksumSha256 !== review.audit.checksumSha256 ||
    review.session.evidence.coverageChecksumSha256 !== review.coverage.checksumSha256 ||
    review.session.evidence.preflightPassed !== review.audit.preflightPassed ||
    review.session.evidence.contractPassed !== review.audit.reviewContract.passed
  ) {
    fail(
      "proof-graph-review-mismatch",
      "ProofGraph review artifacts do not match their immutable session."
    );
  }
  if (!Array.isArray(review.dispositions) || review.dispositions.length > 2_000) {
    fail(
      "proof-graph-disposition-boundary",
      "ProofGraph dispositions must be a bounded array."
    );
  }
  let previousChecksum = null;
  const findingIds = new Set(
    review.audit.decision.findings.map((finding) => finding.id)
  );
  for (const disposition of review.dispositions) {
    verifyRunrailReviewDisposition(disposition);
    if (
      disposition.reviewId !== review.session.reviewId ||
      disposition.repoFingerprint !== review.session.repoFingerprint ||
      disposition.auditChecksumSha256 !== review.audit.checksumSha256 ||
      !findingIds.has(disposition.findingId) ||
      disposition.previousDispositionChecksumSha256 !== previousChecksum
    ) {
      fail(
        "proof-graph-disposition-mismatch",
        "ProofGraph disposition chain does not belong to its review session."
      );
    }
    previousChecksum = disposition.checksumSha256;
  }
  return review;
}

function orderVerifiedReviewLineage(reviews) {
  const byReviewId = new Map();
  for (const review of reviews) {
    const reviewId = review.session.reviewId;
    if (byReviewId.has(reviewId)) {
      fail(
        "proof-graph-lineage-invalid",
        "ProofGraph does not accept duplicate review sessions."
      );
    }
    byReviewId.set(reviewId, review);
  }

  const roots = [];
  const childByParent = new Map();
  for (const review of reviews) {
    const parentReviewId = review.session.parentReviewId;
    if (parentReviewId === null) {
      roots.push(review);
      continue;
    }
    if (!byReviewId.has(parentReviewId) || childByParent.has(parentReviewId)) {
      fail(
        "proof-graph-lineage-invalid",
        "ProofGraph reviews must form one complete non-branching parent lineage."
      );
    }
    childByParent.set(parentReviewId, review);
  }
  if (roots.length !== 1) {
    fail(
      "proof-graph-lineage-invalid",
      "ProofGraph reviews must contain exactly one lineage root."
    );
  }

  const ordered = [];
  const seen = new Set();
  let current = roots[0];
  while (current) {
    if (seen.has(current.session.reviewId)) {
      fail("proof-graph-lineage-invalid", "ProofGraph detected a parent cycle.");
    }
    seen.add(current.session.reviewId);
    ordered.push(current);
    current = childByParent.get(current.session.reviewId) || null;
  }
  if (ordered.length !== reviews.length) {
    fail(
      "proof-graph-lineage-invalid",
      "ProofGraph reviews must form one complete non-branching parent lineage."
    );
  }
  return ordered;
}

function latestDispositionByFinding(dispositions) {
  const latest = new Map();
  for (const disposition of dispositions) {
    latest.set(disposition.findingId, disposition);
  }
  return latest;
}

function dispositionEffect(kind) {
  if (kind === "useful") return "retain-useful-visible";
  if (kind === "not-relevant") return "mark-not-relevant-visible";
  return "mark-exception-visible";
}

function dispositionCanClassifyFinding(disposition, asOf) {
  return (
    disposition.kind !== "accepted-exception" ||
    isRunrailReviewDispositionActive(disposition, {
      now: new Date(asOf)
    })
  );
}

function relationToPriorReview(prior, current) {
  if (prior.session.local.candidateCommit === current.session.local.baseCommit) {
    return "linear-successor";
  }
  if (prior.session.local.baseCommit === current.session.local.baseCommit) {
    return "same-base";
  }
  fail(
    "proof-graph-lineage-invalid",
    "ProofGraph parent reviews must be linear successors or same-base reviews."
  );
}

export function buildRunrailProofGraph({
  reviews,
  asOf
}) {
  canonicalTimestamp(asOf, "ProofGraph asOf");
  if (
    !Array.isArray(reviews) ||
    reviews.length < 1 ||
    reviews.length > RUNRAIL_PROOF_GRAPH.maximumReviews
  ) {
    fail(
      "proof-graph-review-boundary",
      `ProofGraph requires 1 to ${RUNRAIL_PROOF_GRAPH.maximumReviews} related reviews.`
    );
  }
  const verified = orderVerifiedReviewLineage(reviews.map(verifyReviewInput));
  const repoFingerprint = verified[0].session.repoFingerprint;
  for (let index = 0; index < verified.length; index += 1) {
    const review = verified[index];
    if (review.session.repoFingerprint !== repoFingerprint) {
      fail(
        "proof-graph-repository-mismatch",
        "ProofGraph accepts one local repository lineage at a time."
      );
    }
    const expectedParent = index === 0 ? null : verified[index - 1].session.reviewId;
    if (review.session.parentReviewId !== expectedParent) {
      fail(
        "proof-graph-lineage-invalid",
        "ProofGraph reviews must form one verified parent lineage."
      );
    }
  }

  const nodeMap = new Map();
  const edgeMap = new Map();
  const controls = [];
  const revisionNodes = [];
  const previousFindings = new Map();
  const previousHunks = new Map();
  const inputRecords = [];

  for (let index = 0; index < verified.length; index += 1) {
    const { session, audit, coverage, dispositions } = verified[index];
    const relationToParent =
      index === 0
        ? null
        : relationToPriorReview(verified[index - 1], verified[index]);
    const revisionId = graphNodeId(
      "REV",
      `runrail-proof-graph-revision-v1\0${session.reviewId}\0${audit.checksumSha256}`
    );
    const verdict = evaluateRunrailReviewSession(
      session,
      dispositions,
      { now: new Date(asOf) }
    ).status;
    addNode(nodeMap, {
      id: revisionId,
      type: "revision",
      label: `Review ${index + 1}`,
      attributes: {
        ordinal: index + 1,
        verdict,
        preflightPassed: audit.preflightPassed,
        contractPassed: audit.reviewContract.passed,
        matchedRuleCount: audit.reviewContract.matchedRuleCount,
        riskLevel: audit.decision.riskLevel,
        relationToParent,
        attentionRequiredCount:
          coverage.summary.unreviewedCount +
          coverage.summary.needsRevisionCount
      }
    });
    revisionNodes.push(revisionId);
    if (index > 0) {
      addEdge(
        edgeMap,
        "follows-review",
        revisionId,
        revisionNodes[index - 1]
      );
      if (relationToParent === "linear-successor") {
        addEdge(
          edgeMap,
          "supersedes-revision",
          revisionId,
          revisionNodes[index - 1]
        );
      }
    }

    const contractId = graphNodeId(
      "CON",
      `runrail-proof-graph-contract-v1\0${audit.reviewContract.contractSha256}\0${audit.reviewContract.source}`
    );
    addNode(nodeMap, {
      id: contractId,
      type: "contract",
      label: "Base-owned review contract",
      attributes: {
        contractFingerprintSha256: audit.reviewContract.contractSha256,
        contractSource: audit.reviewContract.source
      }
    });
    addEdge(edgeMap, "evaluated-under", revisionId, contractId);

    for (const match of audit.reviewContract.matches) {
      const ruleId = graphNodeId(
        "RUL",
        `runrail-proof-graph-rule-v1\0${audit.checksumSha256}\0${audit.reviewContract.contractSha256}\0${match.ruleId}`
      );
      addNode(nodeMap, {
        id: ruleId,
        type: "rule",
        label: "Base-contract rule",
        attributes: {
          ruleId: match.ruleId,
          severity: match.severity,
          reviewLane: match.reviewLane,
          missingCompanionGroupCount: match.missingCompanionGroupCount
        }
      });
      addEdge(edgeMap, "matched-rule", contractId, ruleId);
      controls.push({
        controlType: "base-contract-rule",
        targetNodeId: ruleId,
        basisNodeId: contractId,
        effect: "retain-rule-visible",
        hidden: false,
        policyRewritten: false
      });
      for (const evidenceType of match.requiredEvidence) {
        const evidenceId = graphNodeId(
          "EVD",
          `runrail-proof-graph-evidence-v1\0${audit.reviewContract.contractSha256}\0${evidenceType}`
        );
        addNode(nodeMap, {
          id: evidenceId,
          type: "evidence",
          label: "Required review evidence",
          attributes: {
            evidenceType,
            status: "required-not-proven"
          }
        });
        addEdge(edgeMap, "requires-evidence", ruleId, evidenceId);
      }
    }
    for (const evidenceType of audit.reviewContract.requiredEvidence) {
      const evidenceId = graphNodeId(
        "EVD",
        `runrail-proof-graph-evidence-v1\0${audit.reviewContract.contractSha256}\0${evidenceType}`
      );
      addNode(nodeMap, {
        id: evidenceId,
        type: "evidence",
        label: "Required review evidence",
        attributes: {
          evidenceType,
          status: "required-not-proven"
        }
      });
      addEdge(edgeMap, "requires-evidence", revisionId, evidenceId);
    }

    const latestDisposition = latestDispositionByFinding(dispositions);
    const dispositionNodeByChecksum = new Map();
    const findingNodeIds = [];
    for (const disposition of dispositions) {
      const dispositionId = graphNodeId(
        "DSP",
        `runrail-proof-graph-disposition-v1\0${disposition.checksumSha256}`
      );
      addNode(nodeMap, {
        id: dispositionId,
        type: "disposition",
        label: "Explicit finding disposition",
        attributes: {
          kind: disposition.kind,
          active:
            latestDisposition.get(disposition.findingId)?.checksumSha256 ===
              disposition.checksumSha256 &&
            dispositionCanClassifyFinding(disposition, asOf),
          expiresAt: disposition.expiresAt,
          reasonRecorded: disposition.reason !== null,
          ownerRecorded: disposition.owner !== null,
          contentExported: false
        }
      });
      dispositionNodeByChecksum.set(disposition.checksumSha256, dispositionId);
      if (disposition.previousDispositionChecksumSha256) {
        addEdge(
          edgeMap,
          "supersedes-disposition",
          dispositionId,
          dispositionNodeByChecksum.get(
            disposition.previousDispositionChecksumSha256
          )
        );
      }
    }

    for (const finding of audit.decision.findings) {
      const findingId = graphNodeId(
        "FND",
        `runrail-proof-graph-finding-v1\0${session.reviewId}\0${audit.checksumSha256}\0${finding.id}`
      );
      const latestRecordedDisposition =
        latestDisposition.get(finding.id) || null;
      const disposition =
        latestRecordedDisposition &&
        dispositionCanClassifyFinding(latestRecordedDisposition, asOf)
          ? latestRecordedDisposition
          : null;
      addNode(nodeMap, {
        id: findingId,
        type: "finding",
        label: "Deterministic finding",
        attributes: {
          findingId: finding.id,
          severity: finding.severity,
          latestDispositionKind: disposition?.kind || null,
          visibility: "visible"
        }
      });
      findingNodeIds.push(findingId);
      addEdge(edgeMap, "reported-finding", revisionId, findingId);
      if (disposition) {
        const dispositionId = dispositionNodeByChecksum.get(
          disposition.checksumSha256
        );
        addEdge(edgeMap, "classifies-finding", dispositionId, findingId);
        controls.push({
          controlType: "finding-disposition",
          targetNodeId: findingId,
          basisNodeId: dispositionId,
          effect: dispositionEffect(disposition.kind),
          hidden: false,
          policyRewritten: false
        });
      }
      const priorFindingId = previousFindings.get(finding.id);
      if (priorFindingId) {
        addEdge(edgeMap, "recurs-from", findingId, priorFindingId);
      }
      previousFindings.set(finding.id, findingId);
    }

    for (const hunk of coverage.hunks) {
      const hunkId = graphNodeId(
        "HNK",
        `runrail-proof-graph-hunk-v1\0${session.reviewId}\0${audit.checksumSha256}\0${hunk.hunkId}`
      );
      addNode(nodeMap, {
        id: hunkId,
        type: "hunk",
        label: "Changed hunk evidence",
        attributes: {
          hunkId: hunk.hunkId,
          status: hunk.status,
          addedLineCount: hunk.addedLineCount,
          removedLineCount: hunk.removedLineCount,
          contextLineCount: hunk.contextLineCount
        }
      });
      addEdge(edgeMap, "contains-hunk", revisionId, hunkId);
      const priorHunkId = previousHunks.get(hunk.continuitySha256);
      if (priorHunkId) {
        addEdge(edgeMap, "continues-hunk", hunkId, priorHunkId);
      }
      previousHunks.set(hunk.continuitySha256, hunkId);
    }

    inputRecords.push({
      revisionNodeId: revisionId,
      auditChecksumSha256: audit.checksumSha256,
      coverageChecksumSha256: coverage.checksumSha256,
      dispositionChecksumsSha256: dispositions.map(
        (disposition) => disposition.checksumSha256
      ),
      findingNodeIds
    });
  }

  const inputSetChecksumSha256 = sha256(
    stableRunrailProofGraphJson(inputRecords)
  );
  const proofGraphId = `RPG-${sha256(
    `runrail-proof-graph-v1\0${inputSetChecksumSha256}\0${asOf}`
  )
    .slice(0, 24)
    .toUpperCase()}`;
  const nodes = [...nodeMap.values()].sort((left, right) =>
    compareText(left.id, right.id)
  );
  const edges = [...edgeMap.values()].sort((left, right) =>
    compareText(left.id, right.id)
  );
  const noiseControls = controls.sort((left, right) =>
    compareText(
      `${left.controlType}\0${left.targetNodeId}\0${left.basisNodeId}`,
      `${right.controlType}\0${right.targetNodeId}\0${right.basisNodeId}`
    )
  );
  const body = {
    schemaVersion: RUNRAIL_PROOF_GRAPH.schemaVersion,
    artifactType: RUNRAIL_PROOF_GRAPH.artifactType,
    profile: RUNRAIL_PROOF_GRAPH.profile,
    product: RUNRAIL_PROOF_GRAPH.product,
    proofGraphId,
    asOf,
    reviewCount: verified.length,
    inputSetChecksumSha256,
    inputs: inputRecords,
    nodes,
    edges,
    noiseControls: {
      mode: "explicit-dispositions-and-base-contract-only",
      sources: ["base-commit-review-contract", "verified-disposition"],
      controls: noiseControls,
      findingOccurrenceInputCount: verified.reduce(
        (total, review) => total + review.audit.decision.findings.length,
        0
      ),
      findingOccurrenceOutputCount: nodes.filter(
        (node) => node.type === "finding"
      ).length,
      suppressedCount: 0,
      hiddenFindingCount: 0,
      opaquePersonalization: false,
      silentSuppression: false,
      automaticPolicyLearning: false
    },
    summary: {
      nodeCount: nodes.length,
      edgeCount: edges.length,
      revisionCount: nodes.filter((node) => node.type === "revision").length,
      contractCount: nodes.filter((node) => node.type === "contract").length,
      ruleCount: nodes.filter((node) => node.type === "rule").length,
      evidenceCount: nodes.filter((node) => node.type === "evidence").length,
      findingCount: nodes.filter((node) => node.type === "finding").length,
      hunkCount: nodes.filter((node) => node.type === "hunk").length,
      dispositionCount: nodes.filter((node) => node.type === "disposition").length
    },
    boundaries: {
      localOnly: true,
      pathFree: true,
      repositoryIdentityIncluded: false,
      branchNamesIncluded: false,
      commitsIncluded: false,
      sourceContentIncluded: false,
      diffContentIncluded: false,
      rawVerifierOutputIncluded: false,
      dispositionReasonIncluded: false,
      dispositionOwnerIncluded: false,
      findingsHidden: false,
      policyMutation: false,
      sourceMutation: false,
      projectTestsExecuted: false,
      networkUsed: false,
      modelAuthority: "none",
      executionAuthority: "none",
      originAuthenticated: false,
      mergeReady: false,
      humanDecisionRequired: true
    }
  };
  return verifyRunrailProofGraph({
    ...body,
    checksumSha256: computeRunrailProofGraphChecksum(body)
  });
}

function verifyNodeAttributes(node) {
  if (node.type === "revision") {
    exactKeys(
      node.attributes,
      [
        "ordinal",
        "verdict",
        "preflightPassed",
        "contractPassed",
        "matchedRuleCount",
        "riskLevel",
        "relationToParent",
        "attentionRequiredCount"
      ],
      "ProofGraph revision attributes"
    );
    expectInteger(node.attributes.ordinal, "ProofGraph revision ordinal", 1, 20);
    if (
      !new Set([
        "blocked",
        "needs-attention",
        "ready-for-human-decision"
      ]).has(node.attributes.verdict) ||
      !new Set(["low", "medium", "high"]).has(node.attributes.riskLevel) ||
      (
        node.attributes.relationToParent !== null &&
        !new Set(["linear-successor", "same-base"]).has(
          node.attributes.relationToParent
        )
      ) ||
      (
        node.attributes.ordinal === 1
          ? node.attributes.relationToParent !== null
          : node.attributes.relationToParent === null
      )
    ) {
      fail("proof-graph-invalid", "ProofGraph revision attributes are invalid.");
    }
    expectBoolean(node.attributes.preflightPassed, "ProofGraph revision preflight");
    expectBoolean(node.attributes.contractPassed, "ProofGraph revision contract");
    expectInteger(
      node.attributes.matchedRuleCount,
      "ProofGraph revision matched rule count",
      0,
      32
    );
    expectInteger(
      node.attributes.attentionRequiredCount,
      "ProofGraph revision attention",
      0,
      10_000
    );
    return;
  }
  if (node.type === "contract") {
    exactKeys(
      node.attributes,
      [
        "contractFingerprintSha256",
        "contractSource"
      ],
      "ProofGraph contract attributes"
    );
    if (
      !SHA256_PATTERN.test(String(node.attributes.contractFingerprintSha256 || "")) ||
      !new Set(["built-in", "baseline-file"]).has(node.attributes.contractSource) ||
      node.id !==
        graphNodeId(
          "CON",
          `runrail-proof-graph-contract-v1\0${node.attributes.contractFingerprintSha256}\0${node.attributes.contractSource}`
        )
    ) {
      fail("proof-graph-invalid", "ProofGraph contract attributes are invalid.");
    }
    return;
  }
  if (node.type === "rule") {
    exactKeys(
      node.attributes,
      ["ruleId", "severity", "reviewLane", "missingCompanionGroupCount"],
      "ProofGraph rule attributes"
    );
    if (
      !/^[a-z][a-z0-9-]{2,47}$/.test(String(node.attributes.ruleId || "")) ||
      !new Set(["medium", "high"]).has(node.attributes.severity) ||
      !new Set([
        "api",
        "data",
        "delivery",
        "dependency",
        "documentation",
        "policy",
        "product",
        "quality",
        "security"
      ]).has(node.attributes.reviewLane)
    ) {
      fail("proof-graph-invalid", "ProofGraph rule attributes are invalid.");
    }
    expectInteger(
      node.attributes.missingCompanionGroupCount,
      "ProofGraph missing companion groups",
      0,
      4
    );
    return;
  }
  if (node.type === "evidence") {
    exactKeys(
      node.attributes,
      ["evidenceType", "status"],
      "ProofGraph evidence attributes"
    );
    if (
      !new Set([
        "api-compatibility-review",
        "data-migration-review",
        "delivery-review",
        "dependency-review",
        "documentation-review",
        "policy-owner-review",
        "product-owner-review",
        "project-tests",
        "rollback-review",
        "security-review"
      ]).has(node.attributes.evidenceType) ||
      node.attributes.status !== "required-not-proven"
    ) {
      fail("proof-graph-invalid", "ProofGraph evidence attributes are invalid.");
    }
    return;
  }
  if (node.type === "finding") {
    exactKeys(
      node.attributes,
      [
        "findingId",
        "severity",
        "latestDispositionKind",
        "visibility"
      ],
      "ProofGraph finding attributes"
    );
    if (
      !/^[A-Z][A-Z0-9_]{2,63}$/.test(String(node.attributes.findingId || "")) ||
      !findingSeverities.has(node.attributes.severity) ||
      (
        node.attributes.latestDispositionKind !== null &&
        !dispositionKinds.has(node.attributes.latestDispositionKind)
      ) ||
      node.attributes.visibility !== "visible"
    ) {
      fail("proof-graph-invalid", "ProofGraph finding attributes are invalid.");
    }
    return;
  }
  if (node.type === "hunk") {
    exactKeys(
      node.attributes,
      [
        "hunkId",
        "status",
        "addedLineCount",
        "removedLineCount",
        "contextLineCount"
      ],
      "ProofGraph hunk attributes"
    );
    if (
      !/^HNK-[A-F0-9]{24}$/.test(String(node.attributes.hunkId || "")) ||
      !hunkStatuses.has(node.attributes.status)
    ) {
      fail("proof-graph-invalid", "ProofGraph hunk attributes are invalid.");
    }
    for (const key of [
      "addedLineCount",
      "removedLineCount",
      "contextLineCount"
    ]) {
      expectInteger(node.attributes[key], `ProofGraph hunk ${key}`, 0, 10_000);
    }
    return;
  }
  exactKeys(
    node.attributes,
    [
      "kind",
      "active",
      "expiresAt",
      "reasonRecorded",
      "ownerRecorded",
      "contentExported"
    ],
    "ProofGraph disposition attributes"
  );
  if (!dispositionKinds.has(node.attributes.kind)) {
    fail("proof-graph-invalid", "ProofGraph disposition kind is invalid.");
  }
  expectBoolean(node.attributes.active, "ProofGraph disposition active state");
  canonicalTimestamp(
    node.attributes.expiresAt,
    "ProofGraph disposition expiry",
    { nullable: true }
  );
  for (const key of ["reasonRecorded", "ownerRecorded", "contentExported"]) {
    expectBoolean(node.attributes[key], `ProofGraph disposition ${key}`);
  }
  if (node.attributes.contentExported !== false) {
    fail("proof-graph-invalid", "ProofGraph exported disposition content.");
  }
}

export function verifyRunrailProofGraph(graph) {
  exactKeys(
    graph,
    [
      "schemaVersion",
      "artifactType",
      "profile",
      "product",
      "proofGraphId",
      "asOf",
      "reviewCount",
      "inputSetChecksumSha256",
      "inputs",
      "nodes",
      "edges",
      "noiseControls",
      "summary",
      "boundaries",
      "checksumSha256"
    ],
    "ProofGraph"
  );
  if (
    graph.schemaVersion !== RUNRAIL_PROOF_GRAPH.schemaVersion ||
    graph.artifactType !== RUNRAIL_PROOF_GRAPH.artifactType ||
    graph.profile !== RUNRAIL_PROOF_GRAPH.profile ||
    graph.product !== RUNRAIL_PROOF_GRAPH.product ||
    !GRAPH_ID_PATTERN.test(String(graph.proofGraphId || "")) ||
    !SHA256_PATTERN.test(String(graph.inputSetChecksumSha256 || "")) ||
    !SHA256_PATTERN.test(String(graph.checksumSha256 || ""))
  ) {
    fail("proof-graph-invalid", "ProofGraph identity is invalid.");
  }
  canonicalTimestamp(graph.asOf, "ProofGraph asOf");
  expectInteger(
    graph.reviewCount,
    "ProofGraph review count",
    1,
    RUNRAIL_PROOF_GRAPH.maximumReviews
  );
  if (
    !Array.isArray(graph.inputs) ||
    graph.inputs.length !== graph.reviewCount
  ) {
    fail("proof-graph-invalid", "ProofGraph input evidence is incomplete.");
  }
  if (
    !Array.isArray(graph.nodes) ||
    graph.nodes.length < 1 ||
    graph.nodes.length > RUNRAIL_PROOF_GRAPH.maximumNodes ||
    !Array.isArray(graph.edges) ||
    graph.edges.length > RUNRAIL_PROOF_GRAPH.maximumEdges
  ) {
    fail("proof-graph-size-boundary", "ProofGraph node or edge boundary is invalid.");
  }
  const nodeIds = new Set();
  let previousNodeId = "";
  for (const node of graph.nodes) {
    exactKeys(node, ["id", "type", "label", "attributes"], "ProofGraph node");
    if (
      !NODE_ID_PATTERN.test(String(node.id || "")) ||
      !nodeTypes.has(node.type) ||
      typeof node.label !== "string" ||
      node.label.length < 1 ||
      node.label.length > 80 ||
      nodeIds.has(node.id) ||
      (previousNodeId && compareText(previousNodeId, node.id) > 0)
    ) {
      fail("proof-graph-invalid", "ProofGraph node identity or order is invalid.");
    }
    nodeIds.add(node.id);
    previousNodeId = node.id;
    verifyNodeAttributes(node);
  }
  const edgeIds = new Set();
  let previousEdgeId = "";
  for (const edge of graph.edges) {
    exactKeys(edge, ["id", "type", "from", "to"], "ProofGraph edge");
    if (
      !EDGE_ID_PATTERN.test(String(edge.id || "")) ||
      !edgeTypes.has(edge.type) ||
      !nodeIds.has(edge.from) ||
      !nodeIds.has(edge.to) ||
      edge.from === edge.to ||
      edge.id !== graphEdgeId(edge.type, edge.from, edge.to) ||
      edgeIds.has(edge.id) ||
      (previousEdgeId && compareText(previousEdgeId, edge.id) > 0)
    ) {
      fail("proof-graph-invalid", "ProofGraph edge identity or order is invalid.");
    }
    edgeIds.add(edge.id);
    previousEdgeId = edge.id;
  }
  const revisionsByOrdinal = graph.nodes
    .filter((node) => node.type === "revision")
    .sort((left, right) => left.attributes.ordinal - right.attributes.ordinal);
  if (
    revisionsByOrdinal.length !== graph.reviewCount ||
    revisionsByOrdinal.some(
      (node, index) => node.attributes.ordinal !== index + 1
    )
  ) {
    fail("proof-graph-invalid", "ProofGraph revision ordinals are incomplete.");
  }
  const reviewSequenceEdges = graph.edges.filter(
    (edge) => edge.type === "follows-review"
  );
  const supersessionEdges = graph.edges.filter(
    (edge) => edge.type === "supersedes-revision"
  );
  const expectedSupersessionIds = new Set();
  if (reviewSequenceEdges.length !== Math.max(0, graph.reviewCount - 1)) {
    fail("proof-graph-invalid", "ProofGraph review sequence is incomplete.");
  }
  for (let index = 1; index < revisionsByOrdinal.length; index += 1) {
    const current = revisionsByOrdinal[index];
    const prior = revisionsByOrdinal[index - 1];
    const follows = graphEdgeId("follows-review", current.id, prior.id);
    const supersedes = graphEdgeId(
      "supersedes-revision",
      current.id,
      prior.id
    );
    if (!edgeIds.has(follows)) {
      fail("proof-graph-invalid", "ProofGraph review sequence is invalid.");
    }
    if (
      edgeIds.has(supersedes) !==
      (current.attributes.relationToParent === "linear-successor")
    ) {
      fail("proof-graph-invalid", "ProofGraph revision relationship is invalid.");
    }
    if (current.attributes.relationToParent === "linear-successor") {
      expectedSupersessionIds.add(supersedes);
    }
  }
  if (
    supersessionEdges.length !== expectedSupersessionIds.size ||
    supersessionEdges.some(
      (edge) =>
        !expectedSupersessionIds.has(edge.id)
    )
  ) {
    fail("proof-graph-invalid", "ProofGraph supersession endpoints are invalid.");
  }
  const committedFindingNodeIds = new Set();
  for (let index = 0; index < graph.inputs.length; index += 1) {
    const input = graph.inputs[index];
    exactKeys(
      input,
      [
        "revisionNodeId",
        "auditChecksumSha256",
        "coverageChecksumSha256",
        "dispositionChecksumsSha256",
        "findingNodeIds"
      ],
      "ProofGraph input evidence"
    );
    const expectedRevision = revisionsByOrdinal[index];
    if (
      input.revisionNodeId !== expectedRevision?.id ||
      !SHA256_PATTERN.test(String(input.auditChecksumSha256 || "")) ||
      !SHA256_PATTERN.test(String(input.coverageChecksumSha256 || "")) ||
      !Array.isArray(input.dispositionChecksumsSha256) ||
      input.dispositionChecksumsSha256.length > 2_000 ||
      input.dispositionChecksumsSha256.some(
        (checksum) => !SHA256_PATTERN.test(String(checksum || ""))
      ) ||
      !Array.isArray(input.findingNodeIds) ||
      input.findingNodeIds.length > 100 ||
      input.findingNodeIds.some(
        (nodeId) =>
          !NODE_ID_PATTERN.test(String(nodeId || "")) ||
          committedFindingNodeIds.has(nodeId) ||
          graph.nodes.find((node) => node.id === nodeId)?.type !== "finding"
      )
    ) {
      fail("proof-graph-invalid", "ProofGraph input evidence is invalid.");
    }
    for (const nodeId of input.findingNodeIds) {
      committedFindingNodeIds.add(nodeId);
    }
  }
  const graphFindingNodeIds = new Set(
    graph.nodes
      .filter((node) => node.type === "finding")
      .map((node) => node.id)
  );
  if (
    committedFindingNodeIds.size !== graphFindingNodeIds.size ||
    [...graphFindingNodeIds].some(
      (nodeId) => !committedFindingNodeIds.has(nodeId)
    )
  ) {
    fail(
      "proof-graph-noise-control",
      "ProofGraph finding occurrences do not match committed input evidence."
    );
  }
  const expectedInputSetChecksumSha256 = sha256(
    stableRunrailProofGraphJson(graph.inputs)
  );
  if (
    graph.inputSetChecksumSha256 !== expectedInputSetChecksumSha256 ||
    graph.proofGraphId !==
      `RPG-${sha256(
        `runrail-proof-graph-v1\0${expectedInputSetChecksumSha256}\0${graph.asOf}`
      )
        .slice(0, 24)
        .toUpperCase()}`
  ) {
    fail("proof-graph-invalid", "ProofGraph input binding is invalid.");
  }
  exactKeys(
    graph.noiseControls,
    [
      "mode",
      "sources",
      "controls",
      "findingOccurrenceInputCount",
      "findingOccurrenceOutputCount",
      "suppressedCount",
      "hiddenFindingCount",
      "opaquePersonalization",
      "silentSuppression",
      "automaticPolicyLearning"
    ],
    "ProofGraph noise controls"
  );
  if (
    graph.noiseControls.mode !==
      "explicit-dispositions-and-base-contract-only" ||
    JSON.stringify(graph.noiseControls.sources) !==
      JSON.stringify(["base-commit-review-contract", "verified-disposition"]) ||
    !Array.isArray(graph.noiseControls.controls) ||
    graph.noiseControls.findingOccurrenceInputCount !==
      committedFindingNodeIds.size ||
    graph.noiseControls.findingOccurrenceOutputCount !==
      graph.nodes.filter((node) => node.type === "finding").length ||
    graph.noiseControls.suppressedCount !== 0 ||
    graph.noiseControls.hiddenFindingCount !== 0 ||
    graph.noiseControls.opaquePersonalization !== false ||
    graph.noiseControls.silentSuppression !== false ||
    graph.noiseControls.automaticPolicyLearning !== false
  ) {
    fail("proof-graph-noise-control", "ProofGraph noise-control boundary is invalid.");
  }
  let previousControlIdentity = "";
  for (const control of graph.noiseControls.controls) {
    exactKeys(
      control,
      [
        "controlType",
        "targetNodeId",
        "basisNodeId",
        "effect",
        "hidden",
        "policyRewritten"
      ],
      "ProofGraph noise control"
    );
    const identity = `${control.controlType}\0${control.targetNodeId}\0${control.basisNodeId}`;
    if (
      !new Set(["finding-disposition", "base-contract-rule"]).has(
        control.controlType
      ) ||
      !nodeIds.has(control.targetNodeId) ||
      !nodeIds.has(control.basisNodeId) ||
      !new Set([
        "retain-useful-visible",
        "mark-not-relevant-visible",
        "mark-exception-visible",
        "retain-rule-visible"
      ]).has(control.effect) ||
      control.hidden !== false ||
      control.policyRewritten !== false ||
      (previousControlIdentity &&
        compareText(previousControlIdentity, identity) > 0)
    ) {
      fail("proof-graph-noise-control", "ProofGraph noise control is invalid.");
    }
    previousControlIdentity = identity;
  }
  exactKeys(
    graph.summary,
    [
      "nodeCount",
      "edgeCount",
      "revisionCount",
      "contractCount",
      "ruleCount",
      "evidenceCount",
      "findingCount",
      "hunkCount",
      "dispositionCount"
    ],
    "ProofGraph summary"
  );
  const expectedSummary = {
    nodeCount: graph.nodes.length,
    edgeCount: graph.edges.length,
    revisionCount: graph.nodes.filter((node) => node.type === "revision").length,
    contractCount: graph.nodes.filter((node) => node.type === "contract").length,
    ruleCount: graph.nodes.filter((node) => node.type === "rule").length,
    evidenceCount: graph.nodes.filter((node) => node.type === "evidence").length,
    findingCount: graph.nodes.filter((node) => node.type === "finding").length,
    hunkCount: graph.nodes.filter((node) => node.type === "hunk").length,
    dispositionCount: graph.nodes.filter((node) => node.type === "disposition").length
  };
  if (stableRunrailProofGraphJson(graph.summary) !== stableRunrailProofGraphJson(expectedSummary)) {
    fail("proof-graph-invalid", "ProofGraph summary is invalid.");
  }
  if (graph.summary.revisionCount !== graph.reviewCount) {
    fail("proof-graph-invalid", "ProofGraph review count does not match revision nodes.");
  }
  exactKeys(
    graph.boundaries,
    [
      "localOnly",
      "pathFree",
      "repositoryIdentityIncluded",
      "branchNamesIncluded",
      "commitsIncluded",
      "sourceContentIncluded",
      "diffContentIncluded",
      "rawVerifierOutputIncluded",
      "dispositionReasonIncluded",
      "dispositionOwnerIncluded",
      "findingsHidden",
      "policyMutation",
      "sourceMutation",
      "projectTestsExecuted",
      "networkUsed",
      "modelAuthority",
      "executionAuthority",
      "originAuthenticated",
      "mergeReady",
      "humanDecisionRequired"
    ],
    "ProofGraph boundaries"
  );
  if (
    graph.boundaries.localOnly !== true ||
    graph.boundaries.pathFree !== true ||
    graph.boundaries.repositoryIdentityIncluded !== false ||
    graph.boundaries.branchNamesIncluded !== false ||
    graph.boundaries.commitsIncluded !== false ||
    graph.boundaries.sourceContentIncluded !== false ||
    graph.boundaries.diffContentIncluded !== false ||
    graph.boundaries.rawVerifierOutputIncluded !== false ||
    graph.boundaries.dispositionReasonIncluded !== false ||
    graph.boundaries.dispositionOwnerIncluded !== false ||
    graph.boundaries.findingsHidden !== false ||
    graph.boundaries.policyMutation !== false ||
    graph.boundaries.sourceMutation !== false ||
    graph.boundaries.projectTestsExecuted !== false ||
    graph.boundaries.networkUsed !== false ||
    graph.boundaries.modelAuthority !== "none" ||
    graph.boundaries.executionAuthority !== "none" ||
    graph.boundaries.originAuthenticated !== false ||
    graph.boundaries.mergeReady !== false ||
    graph.boundaries.humanDecisionRequired !== true
  ) {
    fail("proof-graph-authority", "ProofGraph expanded its authority boundary.");
  }
  const serialized = stableRunrailProofGraphJson(graph);
  if (forbiddenSerializedTerms.some((term) => serialized.includes(term))) {
    fail("proof-graph-source-leak", "ProofGraph contains a forbidden source field.");
  }
  const { checksumSha256, ...body } = graph;
  if (computeRunrailProofGraphChecksum(body) !== checksumSha256) {
    fail("proof-graph-tampered", "ProofGraph checksum verification failed.");
  }
  return graph;
}

async function ensureOrdinaryDirectory(directory, label) {
  const entry = await lstat(directory).catch(() => null);
  if (!entry || !entry.isDirectory() || entry.isSymbolicLink()) {
    fail("proof-graph-storage", `${label} must be an ordinary directory.`);
  }
  const canonical = await realpath(directory);
  if (canonical !== path.resolve(directory)) {
    fail("proof-graph-storage", `${label} must not traverse a symbolic link.`);
  }
  return canonical;
}

export async function writeRunrailProofGraph({ graph, historyRoot }) {
  verifyRunrailProofGraph(graph);
  const canonicalHistoryRoot = await ensureOrdinaryDirectory(
    historyRoot,
    "Runrail history root"
  );
  const outputRoot = path.join(canonicalHistoryRoot, "proofgraphs");
  await mkdir(outputRoot, { recursive: true, mode: 0o700 });
  const canonicalOutputRoot = await ensureOrdinaryDirectory(
    outputRoot,
    "ProofGraph output root"
  );
  if (process.platform !== "win32") await chmod(canonicalOutputRoot, 0o700);
  const proofGraphPath = path.join(
    canonicalOutputRoot,
    `${graph.proofGraphId}.json`
  );
  const expected = stableRunrailProofGraphJson(graph);
  const existing = await lstat(proofGraphPath).catch(() => null);
  if (existing) {
    if (!existing.isFile() || existing.isSymbolicLink() || existing.size > 8 * 1024 * 1024) {
      fail(
        "proof-graph-storage",
        "The existing ProofGraph artifact is not an ordinary bounded file."
      );
    }
    const stored = verifyRunrailProofGraph(
      JSON.parse(await readFile(proofGraphPath, "utf8"))
    );
    if (stableRunrailProofGraphJson(stored) !== expected) {
      fail(
        "proof-graph-conflict",
        "A different ProofGraph already exists for this input lineage and as-of time."
      );
    }
    return { proofGraphPath, graph: stored };
  }
  const handle = await open(proofGraphPath, "wx", 0o600);
  try {
    await handle.writeFile(expected, "utf8");
    await handle.sync();
  } finally {
    await handle.close();
  }
  if (process.platform !== "win32") await chmod(proofGraphPath, 0o600);
  const stored = verifyRunrailProofGraph(
    JSON.parse(await readFile(proofGraphPath, "utf8"))
  );
  return { proofGraphPath, graph: stored };
}

async function readSessionArtifact({
  session,
  filePath,
  expectedFileSha256,
  maximumBytes,
  label
}) {
  const artifactRoot = path.resolve(session.local.artifactRootPath);
  const rootEntry = await lstat(artifactRoot).catch(() => null);
  if (!rootEntry || !rootEntry.isDirectory() || rootEntry.isSymbolicLink()) {
    fail("proof-graph-artifact-invalid", `${label} root is invalid.`);
  }
  const canonicalRoot = await realpath(artifactRoot);
  if (canonicalRoot !== artifactRoot) {
    fail("proof-graph-artifact-invalid", `${label} root traverses a symbolic link.`);
  }
  const normalizedPath = path.resolve(filePath);
  if (
    normalizedPath === canonicalRoot ||
    !isInsideOrEqual(normalizedPath, canonicalRoot)
  ) {
    fail("proof-graph-artifact-boundary", `${label} escaped its artifact root.`);
  }
  const entry = await lstat(normalizedPath).catch(() => null);
  if (
    !entry ||
    !entry.isFile() ||
    entry.isSymbolicLink() ||
    entry.size < 2 ||
    entry.size > maximumBytes
  ) {
    fail("proof-graph-artifact-invalid", `${label} is not an ordinary bounded file.`);
  }
  const canonicalPath = await realpath(normalizedPath);
  if (
    canonicalPath !== normalizedPath ||
    !isInsideOrEqual(canonicalPath, canonicalRoot)
  ) {
    fail("proof-graph-artifact-boundary", `${label} traverses a symbolic link.`);
  }
  const bytes = await readFile(canonicalPath);
  if (
    !SHA256_PATTERN.test(String(expectedFileSha256 || "")) ||
    sha256(bytes) !== expectedFileSha256
  ) {
    fail("proof-graph-artifact-tampered", `${label} file checksum changed.`);
  }
  try {
    return JSON.parse(bytes.toString("utf8"));
  } catch {
    fail("proof-graph-artifact-invalid", `${label} is not valid JSON.`);
  }
}

export async function loadRunrailProofGraph({
  historyRoot,
  reviewId,
  asOf
}) {
  canonicalTimestamp(asOf, "ProofGraph asOf");
  const lineageNewestFirst = [];
  const seen = new Set();
  let session = await findRunrailReviewSession({ historyRoot, reviewId });
  while (session) {
    verifyRunrailReviewSession(session);
    if (seen.has(session.reviewId)) {
      fail("proof-graph-lineage-invalid", "ProofGraph detected a parent cycle.");
    }
    seen.add(session.reviewId);
    lineageNewestFirst.push(session);
    if (lineageNewestFirst.length > RUNRAIL_PROOF_GRAPH.maximumReviews) {
      fail(
        "proof-graph-review-boundary",
        `ProofGraph accepts at most ${RUNRAIL_PROOF_GRAPH.maximumReviews} related reviews.`
      );
    }
    session = session.parentReviewId
      ? await findRunrailReviewSession({
          historyRoot,
          reviewId: session.parentReviewId
        })
      : null;
  }
  const reviews = [];
  for (const current of lineageNewestFirst.reverse()) {
    const audit = await readSessionArtifact({
      session: current,
      filePath: current.local.auditPath,
      expectedFileSha256: current.evidence.auditFileSha256,
      maximumBytes: 2 * 1024 * 1024,
      label: "ProofGraph audit"
    });
    const coverage = await readSessionArtifact({
      session: current,
      filePath: current.local.coveragePath,
      expectedFileSha256: current.evidence.coverageFileSha256,
      maximumBytes: 4 * 1024 * 1024,
      label: "ProofGraph coverage"
    });
    const dispositions = await listRunrailReviewDispositions({
      historyRoot,
      repoFingerprint: current.repoFingerprint,
      reviewId: current.reviewId
    });
    reviews.push({ session: current, audit, coverage, dispositions });
  }
  const graph = buildRunrailProofGraph({ reviews, asOf });
  return writeRunrailProofGraph({ graph, historyRoot });
}
