import { createHash } from "node:crypto";
import { isDeepStrictEqual } from "node:util";
import { verifyRunrailCandidateAudit } from "./runrail-deepseek-advisory-core.mjs";
import { compareRunrailReviewAudits } from "./runrail-review-comparison-core.mjs";

const riskRank = Object.freeze({ low: 0, medium: 1, high: 2 });
const relationTypes = new Set(["same-base", "linear-successor"]);
const directionTypes = new Set([
  "decreased",
  "increased",
  "changed",
  "unchanged",
  "policy-basis-changed"
]);

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

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

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

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 stableRunrailEvidenceContinuityJson(value) {
  return `${JSON.stringify(stableValue(value), null, 2)}\n`;
}

function exactKeys(value, keys) {
  return (
    isRecord(value) &&
    Object.keys(value).sort().join("\0") === [...keys].sort().join("\0")
  );
}

function boundedInteger(value, minimum, maximum) {
  return Number.isSafeInteger(value) && value >= minimum && value <= maximum;
}

function validSha256(value) {
  return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
}

function validRunId(value) {
  return typeof value === "string" && /^RRV-[A-F0-9]{16}-[a-f0-9]{8}$/.test(value);
}

function findingSummary(finding) {
  return {
    id: finding.id,
    severity: finding.severity,
    title: finding.title
  };
}

function auditEntryBody(audit, position, relationToPrevious, previousEntryLinkSha256) {
  const findings = audit.decision.findings
    .map(findingSummary)
    .sort((left, right) => left.id.localeCompare(right.id));
  return {
    position,
    runId: audit.runId,
    auditChecksumSha256: audit.checksumSha256,
    relationToPrevious,
    outcome: {
      preflightPassed: audit.preflightPassed,
      verifierPassed: audit.verifier.passed,
      reviewContractPassed: audit.schemaVersion >= 4 ? audit.reviewContract.passed : null,
      riskLevel: audit.decision.riskLevel,
      recommendation: audit.decision.recommendation
    },
    evidence: {
      changedFileCount: audit.policy.changedFileCount,
      diffLineCount: audit.policy.diffLineCount,
      findingCounts: { ...audit.decision.findingCounts },
      verifierProfile: audit.verifier.profile,
      reviewContractSha256: audit.schemaVersion >= 4 ? audit.reviewContract.contractSha256 : null
    },
    findings,
    previousEntryLinkSha256
  };
}

function linkEntry(entryBody) {
  return sha256(
    `runrail-evidence-continuity-entry-v1\0${stableRunrailEvidenceContinuityJson(entryBody)}`
  );
}

function lineageIdForEntries(entries) {
  const sequenceIdentity = sha256(
    `runrail-evidence-continuity-sequence-v1\0${entries
      .map((entry) => entry.auditChecksumSha256)
      .join("\0")}`
  );
  return [
    "RRL",
    entries[0].auditChecksumSha256.slice(0, 12),
    entries.at(-1).auditChecksumSha256.slice(0, 12),
    sequenceIdentity.slice(0, 32)
  ].join("-").toUpperCase();
}

function transitionSummary(comparison, beforePosition, afterPosition) {
  return {
    beforePosition,
    afterPosition,
    relationship: comparison.relationship.type,
    reviewPriorityDirection: comparison.reviewPriority.direction,
    comparablePolicyBasis: comparison.reviewContractDelta.comparablePolicyBasis,
    introducedFindingIds: comparison.findings.added.map((finding) => finding.id).sort(),
    absentFindingIds: comparison.findings.resolved.map((finding) => finding.id).sort()
  };
}

function deriveFindingLifecycle(entries) {
  const byId = new Map();
  for (const entry of entries) {
    const present = new Set(entry.findings.map((finding) => finding.id));
    for (const finding of entry.findings) {
      const existing = byId.get(finding.id);
      if (
        existing &&
        (existing.title !== finding.title || existing.severity !== finding.severity)
      ) {
        fail(
          "lineage-finding-definition-changed",
          `Finding ${finding.id} changed its deterministic title or severity across the evidence sequence.`
        );
      }
      if (!existing) {
        byId.set(finding.id, {
          ...finding,
          positions: [],
          resolutionCount: 0,
          recurrenceCount: 0,
          previouslyPresent: false,
          seenBefore: false
        });
      }
    }
    for (const state of byId.values()) {
      const isPresent = present.has(state.id);
      if (state.previouslyPresent && !isPresent) state.resolutionCount += 1;
      if (!state.previouslyPresent && isPresent && state.seenBefore) state.recurrenceCount += 1;
      if (isPresent) {
        state.positions.push(entry.position);
        state.seenBefore = true;
      }
      state.previouslyPresent = isPresent;
    }
  }
  const latestPosition = entries.at(-1).position;
  const findings = [...byId.values()]
    .map((state) => ({
      id: state.id,
      severity: state.severity,
      title: state.title,
      firstSeenPosition: state.positions[0],
      lastSeenPosition: state.positions.at(-1),
      occurrenceCount: state.positions.length,
      resolutionCount: state.resolutionCount,
      recurrenceCount: state.recurrenceCount,
      presentInLatest: state.positions.at(-1) === latestPosition
    }))
    .sort((left, right) => left.id.localeCompare(right.id));
  return {
    summary: {
      uniqueFindingCount: findings.length,
      activeInLatestCount: findings.filter((finding) => finding.presentInLatest).length,
      absentFromLatestCount: findings.filter((finding) => !finding.presentInLatest).length,
      recurringFindingCount: findings.filter((finding) => finding.recurrenceCount > 0).length
    },
    findings
  };
}

function deriveSequence(entries, transitions) {
  const contractHashes = entries.map((entry) => entry.evidence.reviewContractSha256);
  const policyBasisContinuous =
    contractHashes.every(validSha256) &&
    new Set(contractHashes).size === 1 &&
    transitions.every((transition) => transition.comparablePolicyBasis);
  const contractChangeCount = contractHashes
    .slice(1)
    .filter((hash, index) => hash !== contractHashes[index])
    .length;
  let reviewPriorityDirection = "policy-basis-changed";
  if (policyBasisContinuous) {
    const before = riskRank[entries[0].outcome.riskLevel];
    const after = riskRank[entries.at(-1).outcome.riskLevel];
    if (after < before) reviewPriorityDirection = "decreased";
    else if (after > before) reviewPriorityDirection = "increased";
    else {
      reviewPriorityDirection = transitions.some(
        (transition) => transition.reviewPriorityDirection !== "unchanged"
      )
        ? "changed"
        : "unchanged";
    }
  }
  return {
    entryCount: entries.length,
    ordering: "operator-declared",
    policyBasisContinuous,
    contractChangeCount,
    reviewPriorityDirection
  };
}

const boundaries = Object.freeze({
  localEvidenceOnly: true,
  pathFree: true,
  sourceContentIncluded: false,
  diffContentIncluded: false,
  changedPathsIncluded: false,
  commitIdentifiersIncluded: false,
  repositoryIdentityIncluded: false,
  rawVerifierOutputIncluded: false,
  originAuthenticated: false,
  orderAuthenticated: false,
  appendOnlyGuaranteed: false,
  networkRequired: false,
  projectTestsExecuted: false,
  securityReviewPerformed: false,
  automaticSourceApplyEnabled: false,
  humanApprovalRequired: true,
  mergeReady: false,
  learnedRuntimeAuthorityAdded: false
});

export function buildRunrailEvidenceContinuity(audits) {
  if (!Array.isArray(audits) || audits.length < 2 || audits.length > 20) {
    fail(
      "lineage-audit-count-invalid",
      "Runrail Evidence Continuity requires between 2 and 20 review audits."
    );
  }
  for (const audit of audits) verifyRunrailCandidateAudit(audit);
  if (
    new Set(audits.map((audit) => audit.checksumSha256)).size !== audits.length ||
    new Set(audits.map((audit) => audit.runId)).size !== audits.length ||
    new Set(audits.map((audit) => audit.source.candidateCommit)).size !== audits.length
  ) {
    fail("lineage-audit-duplicate", "Runrail Evidence Continuity does not accept duplicate review evidence.");
  }

  const comparisons = [];
  for (let index = 1; index < audits.length; index += 1) {
    try {
      comparisons.push(compareRunrailReviewAudits(audits[index - 1], audits[index]));
    } catch (error) {
      fail(
        error?.code === "audit-comparison-not-related"
          ? "lineage-audit-not-related"
          : "lineage-audit-order-invalid",
        `Review positions ${index} and ${index + 1} do not form an accepted declared evidence sequence.`
      );
    }
  }

  const entries = [];
  let previousEntryLinkSha256 = null;
  for (let index = 0; index < audits.length; index += 1) {
    const body = auditEntryBody(
      audits[index],
      index + 1,
      index === 0 ? null : comparisons[index - 1].relationship.type,
      previousEntryLinkSha256
    );
    const entry = { ...body, entryLinkSha256: linkEntry(body) };
    entries.push(entry);
    previousEntryLinkSha256 = entry.entryLinkSha256;
  }
  const transitions = comparisons.map((comparison, index) =>
    transitionSummary(comparison, index + 1, index + 2)
  );
  const body = {
    schemaVersion: 1,
    artifactType: "runrail-evidence-continuity",
    product: "Runrail Local v1",
    lineageId: lineageIdForEntries(entries),
    sequence: deriveSequence(entries, transitions),
    entries,
    transitions,
    findingLifecycle: deriveFindingLifecycle(entries),
    chain: {
      algorithm: "sha256",
      profile: "runrail-evidence-continuity-chain-v1",
      rootSha256: entries.at(-1).entryLinkSha256,
      contentIntegrityOnly: true
    },
    boundaries: { ...boundaries }
  };
  return verifyRunrailEvidenceContinuity({
    ...body,
    checksumSha256: sha256(stableRunrailEvidenceContinuityJson(body))
  });
}

function validateFinding(finding) {
  return (
    exactKeys(finding, ["id", "severity", "title"]) &&
    typeof finding.id === "string" &&
    /^[A-Z][A-Z0-9_]{1,63}$/.test(finding.id) &&
    new Set(["high", "medium", "info"]).has(finding.severity) &&
    typeof finding.title === "string" &&
    finding.title.length >= 1 &&
    finding.title.length <= 160 &&
    !/[\u0000-\u001f\u007f]/.test(finding.title)
  );
}

function validateEntry(entry, position, previousLink) {
  if (
    !exactKeys(entry, [
      "position",
      "runId",
      "auditChecksumSha256",
      "relationToPrevious",
      "outcome",
      "evidence",
      "findings",
      "previousEntryLinkSha256",
      "entryLinkSha256"
    ]) ||
    entry.position !== position ||
    !validRunId(entry.runId) ||
    !validSha256(entry.auditChecksumSha256) ||
    (position === 1 && entry.relationToPrevious !== null) ||
    (position > 1 && !relationTypes.has(entry.relationToPrevious)) ||
    entry.previousEntryLinkSha256 !== previousLink ||
    !validSha256(entry.entryLinkSha256)
  ) {
    fail("lineage-entry-invalid", `Evidence sequence entry ${position} is invalid.`);
  }
  if (
    !exactKeys(entry.outcome, [
      "preflightPassed",
      "verifierPassed",
      "reviewContractPassed",
      "riskLevel",
      "recommendation"
    ]) ||
    typeof entry.outcome.preflightPassed !== "boolean" ||
    typeof entry.outcome.verifierPassed !== "boolean" ||
    ![true, false, null].includes(entry.outcome.reviewContractPassed) ||
    !(entry.outcome.riskLevel in riskRank) ||
    !new Set(["manual-review", "project-tests-required", "specialist-review"]).has(
      entry.outcome.recommendation
    )
  ) {
    fail("lineage-entry-outcome-invalid", `Evidence sequence entry ${position} has an invalid outcome.`);
  }
  if (
    !exactKeys(entry.evidence, [
      "changedFileCount",
      "diffLineCount",
      "findingCounts",
      "verifierProfile",
      "reviewContractSha256"
    ]) ||
    !boundedInteger(entry.evidence.changedFileCount, 1, 100) ||
    !boundedInteger(entry.evidence.diffLineCount, 1, 5_000) ||
    !exactKeys(entry.evidence.findingCounts, ["high", "medium", "info"]) ||
    !["high", "medium", "info"].every((key) =>
      boundedInteger(entry.evidence.findingCounts[key], 0, 20)
    ) ||
    !new Set(["safe-static", "python-compile"]).has(entry.evidence.verifierProfile) ||
    (
      entry.evidence.reviewContractSha256 !== null &&
      !validSha256(entry.evidence.reviewContractSha256)
    )
  ) {
    fail("lineage-entry-evidence-invalid", `Evidence sequence entry ${position} has invalid evidence.`);
  }
  if (
    !Array.isArray(entry.findings) ||
    entry.findings.length > 20 ||
    entry.findings.some((finding) => !validateFinding(finding)) ||
    entry.findings.some(
      (finding, index) => index > 0 && entry.findings[index - 1].id >= finding.id
    )
  ) {
    fail("lineage-entry-findings-invalid", `Evidence sequence entry ${position} has invalid findings.`);
  }
  const { entryLinkSha256, ...entryBody } = entry;
  if (entryLinkSha256 !== linkEntry(entryBody)) {
    fail("lineage-entry-link-invalid", `Evidence sequence entry ${position} has an invalid integrity link.`);
  }
}

function expectedTransition(before, after) {
  const beforeIds = new Set(before.findings.map((finding) => finding.id));
  const afterIds = new Set(after.findings.map((finding) => finding.id));
  return {
    beforePosition: before.position,
    afterPosition: after.position,
    relationship: after.relationToPrevious,
    reviewPriorityDirection: null,
    comparablePolicyBasis:
      validSha256(before.evidence.reviewContractSha256) &&
      before.evidence.reviewContractSha256 === after.evidence.reviewContractSha256,
    introducedFindingIds: [...afterIds].filter((id) => !beforeIds.has(id)).sort(),
    absentFindingIds: [...beforeIds].filter((id) => !afterIds.has(id)).sort()
  };
}

function validateTransition(transition, before, after) {
  const expected = expectedTransition(before, after);
  const rankDelta = riskRank[after.outcome.riskLevel] - riskRank[before.outcome.riskLevel];
  const expectedDirections = !expected.comparablePolicyBasis
    ? new Set(["policy-basis-changed"])
    : rankDelta < 0
      ? new Set(["decreased"])
      : rankDelta > 0
        ? new Set(["increased"])
        : new Set(["changed", "unchanged"]);
  if (
    !exactKeys(transition, [
      "beforePosition",
      "afterPosition",
      "relationship",
      "reviewPriorityDirection",
      "comparablePolicyBasis",
      "introducedFindingIds",
      "absentFindingIds"
    ]) ||
    transition.beforePosition !== expected.beforePosition ||
    transition.afterPosition !== expected.afterPosition ||
    transition.relationship !== expected.relationship ||
    !directionTypes.has(transition.reviewPriorityDirection) ||
    !expectedDirections.has(transition.reviewPriorityDirection) ||
    transition.comparablePolicyBasis !== expected.comparablePolicyBasis ||
    !isDeepStrictEqual(transition.introducedFindingIds, expected.introducedFindingIds) ||
    !isDeepStrictEqual(transition.absentFindingIds, expected.absentFindingIds)
  ) {
    fail(
      "lineage-transition-invalid",
      `Evidence transition ${before.position} to ${after.position} is invalid.`
    );
  }
}

export function verifyRunrailEvidenceContinuity(lineage) {
  if (
    !exactKeys(lineage, [
      "schemaVersion",
      "artifactType",
      "product",
      "lineageId",
      "sequence",
      "entries",
      "transitions",
      "findingLifecycle",
      "chain",
      "boundaries",
      "checksumSha256"
    ])
  ) {
    fail("lineage-schema-invalid", "The Runrail Evidence Continuity field set is invalid.");
  }
  const { checksumSha256, ...body } = lineage;
  if (
    lineage.schemaVersion !== 1 ||
    lineage.artifactType !== "runrail-evidence-continuity" ||
    lineage.product !== "Runrail Local v1" ||
    !/^RRL-[A-F0-9]{12}-[A-F0-9]{12}-[A-F0-9]{32}$/.test(lineage.lineageId || "") ||
    !validSha256(checksumSha256) ||
    checksumSha256 !== sha256(stableRunrailEvidenceContinuityJson(body)) ||
    !Array.isArray(lineage.entries) ||
    lineage.entries.length < 2 ||
    lineage.entries.length > 20 ||
    !Array.isArray(lineage.transitions) ||
    lineage.transitions.length !== lineage.entries.length - 1
  ) {
    fail("lineage-identity-invalid", "The Runrail Evidence Continuity identity or checksum is invalid.");
  }
  if (lineage.lineageId !== lineageIdForEntries(lineage.entries)) {
    fail("lineage-identity-invalid", "The Runrail Evidence Continuity sequence identity is invalid.");
  }
  const runIds = new Set();
  const auditChecksums = new Set();
  let previousLink = null;
  for (let index = 0; index < lineage.entries.length; index += 1) {
    const entry = lineage.entries[index];
    validateEntry(entry, index + 1, previousLink);
    if (runIds.has(entry.runId) || auditChecksums.has(entry.auditChecksumSha256)) {
      fail("lineage-entry-duplicate", "The Runrail Evidence Continuity artifact repeats review evidence.");
    }
    runIds.add(entry.runId);
    auditChecksums.add(entry.auditChecksumSha256);
    previousLink = entry.entryLinkSha256;
  }
  for (let index = 0; index < lineage.transitions.length; index += 1) {
    validateTransition(
      lineage.transitions[index],
      lineage.entries[index],
      lineage.entries[index + 1]
    );
  }

  const expectedSequence = deriveSequence(lineage.entries, lineage.transitions);
  const expectedLifecycle = deriveFindingLifecycle(lineage.entries);
  if (
    !isDeepStrictEqual(lineage.sequence, expectedSequence) ||
    !isDeepStrictEqual(lineage.findingLifecycle, expectedLifecycle)
  ) {
    fail("lineage-derived-evidence-invalid", "The Runrail Evidence Continuity derived evidence is invalid.");
  }
  if (
    !exactKeys(lineage.chain, ["algorithm", "profile", "rootSha256", "contentIntegrityOnly"]) ||
    lineage.chain.algorithm !== "sha256" ||
    lineage.chain.profile !== "runrail-evidence-continuity-chain-v1" ||
    lineage.chain.rootSha256 !== lineage.entries.at(-1).entryLinkSha256 ||
    lineage.chain.contentIntegrityOnly !== true
  ) {
    fail("lineage-chain-invalid", "The Runrail Evidence Continuity chain is invalid.");
  }
  if (!isDeepStrictEqual(lineage.boundaries, boundaries)) {
    fail("lineage-boundary-invalid", "The Runrail Evidence Continuity authority boundary is invalid.");
  }
  return lineage;
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function escapeMarkdown(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll("`", "&#96;");
}

function directionLabel(direction) {
  return {
    decreased: "Review priority decreased",
    increased: "Review priority increased",
    changed: "Review evidence changed",
    unchanged: "Review evidence pattern unchanged",
    "policy-basis-changed": "Review contract basis changed"
  }[direction];
}

export function renderRunrailEvidenceContinuityMarkdown(lineage) {
  verifyRunrailEvidenceContinuity(lineage);
  const lifecycleRows = lineage.findingLifecycle.findings.length > 0
    ? lineage.findingLifecycle.findings
      .map(
        (finding) =>
          `- **${escapeMarkdown(finding.title)}** (\`${finding.id}\`): ${finding.occurrenceCount} occurrence(s), ${finding.recurrenceCount} recurrence(s), ${finding.presentInLatest ? "present" : "absent"} in the latest review.`
      )
      .join("\n")
    : "- No deterministic findings appear in this declared sequence.";
  return `# Runrail Evidence Continuity

Evidence sequence: ${lineage.lineageId}

Declared review count: ${lineage.sequence.entryCount}

Review-priority direction: ${directionLabel(lineage.sequence.reviewPriorityDirection)}

Policy basis continuous: ${lineage.sequence.policyBasisContinuous ? "yes" : "no"}

## Finding lifecycle

${lifecycleRows}

## Integrity

- Content-integrity chain: ${lineage.chain.rootSha256}
- Artifact checksum: ${lineage.checksumSha256}
- Origin authenticated: no
- Order authenticated: no
- Append-only history guaranteed: no

## Decision boundary

- Human approval required: yes
- External project checks required: yes
- Merge ready: no
- Repository upload: disabled
- Automatic source apply: disabled

This artifact verifies the internal content consistency and declared relation of Runrail-owned local review evidence. It does not prove who created the evidence, when it was created, whether the declared order is historical truth, whether the source is semantically correct, or whether a merge should be approved. A changed review-contract basis suppresses any decreased-priority claim.
`;
}

export function renderRunrailEvidenceContinuityHtml(lineage) {
  verifyRunrailEvidenceContinuity(lineage);
  const findings = lineage.findingLifecycle.findings.length > 0
    ? lineage.findingLifecycle.findings
      .map(
        (finding) =>
          `<li><span class="severity severity-${escapeHtml(finding.severity)}">${escapeHtml(finding.severity)}</span><div><strong>${escapeHtml(finding.title)}</strong><small>${escapeHtml(finding.id)} · ${finding.occurrenceCount} occurrence(s) · ${finding.recurrenceCount} recurrence(s) · ${finding.presentInLatest ? "present" : "absent"} in latest</small></div></li>`
      )
      .join("")
    : '<li class="empty">No deterministic findings appear in this declared sequence.</li>';
  return `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Runrail Evidence Continuity</title>
<style>
:root{color-scheme:light;font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#17221d;background:#f2f5f2}
*{box-sizing:border-box}body{margin:0}.shell{max-width:1040px;margin:0 auto;padding:32px 20px 56px}
header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;border-bottom:1px solid #cbd4cd;padding-bottom:24px}
.eyebrow{font-size:12px;font-weight:800;text-transform:uppercase;color:#3d6b55;letter-spacing:.08em}h1{font-size:clamp(30px,5vw,54px);line-height:1;margin:8px 0 12px}
.direction{border:1px solid #97aa9e;background:#fff;padding:12px 16px;font-weight:800;max-width:280px}.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:#cbd4cd;border:1px solid #cbd4cd;margin:24px 0}.grid div{background:#fff;padding:16px}.grid dt{font-size:12px;color:#617066}.grid dd{margin:6px 0 0;font-size:24px;font-weight:800}.panel{background:#fff;border:1px solid #cbd4cd;margin-top:16px;padding:20px}.panel h2{margin-top:0;font-size:20px}ul{list-style:none;padding:0;margin:0}li{display:flex;gap:12px;padding:12px 0;border-top:1px solid #e2e7e3}li:first-child{border-top:0}.severity{align-self:flex-start;padding:4px 7px;border:1px solid #89988f;font-size:11px;text-transform:uppercase;font-weight:800}.severity-high{background:#fee2e2}.severity-medium{background:#fff3cd}.severity-info{background:#e7f1ff}small{display:block;color:#617066;margin-top:4px;overflow-wrap:anywhere}.boundary{border-left:4px solid #3d6b55}code{overflow-wrap:anywhere}@media(max-width:720px){header{display:block}.direction{margin-top:16px}.grid{grid-template-columns:1fr 1fr}}
</style></head><body><main class="shell">
<header><div><p class="eyebrow">Runrail local review evidence</p><h1>Evidence Continuity</h1><p>${escapeHtml(lineage.lineageId)} · ${lineage.sequence.entryCount} operator-declared reviews</p></div>
<div class="direction">${escapeHtml(directionLabel(lineage.sequence.reviewPriorityDirection))}</div></header>
<dl class="grid">
<div><dt>Reviews</dt><dd>${lineage.sequence.entryCount}</dd></div>
<div><dt>Active findings</dt><dd>${lineage.findingLifecycle.summary.activeInLatestCount}</dd></div>
<div><dt>Absent from latest</dt><dd>${lineage.findingLifecycle.summary.absentFromLatestCount}</dd></div>
<div><dt>Recurring findings</dt><dd>${lineage.findingLifecycle.summary.recurringFindingCount}</dd></div>
</dl>
<section class="panel"><h2>Finding lifecycle</h2><ul>${findings}</ul></section>
<section class="panel"><h2>Content-integrity chain</h2><p><code>${escapeHtml(lineage.chain.rootSha256)}</code></p><p>This checksum-linked sequence detects internal content changes. It does not authenticate creator, time, or historical order.</p></section>
<section class="panel boundary"><p class="eyebrow">Decision boundary</p><h2>Human approval and external project checks remain required.</h2><p>${lineage.sequence.policyBasisContinuous ? "The review-contract basis is continuous across the declared sequence." : "The review-contract basis changed, so no decreased-priority claim is made."} This local evidence does not execute project code, upload a repository, apply source changes, perform a security review, authenticate origin, or approve a merge.</p></section>
</main></body></html>`;
}
