///|
/// CapsuleTrace parses and validates capability-boundary manifests.
///
/// A manifest is intentionally line-oriented so it can live in README files,
/// design notes, CI fixtures, and small Wasm demos without external parsers.
pub(all) enum RequirementLevel {
  Must
  Should
  May
} derive(Eq, Debug)

///|
pub(all) enum EvidenceKind {
  Test
  Example
  CI
  Doc
  Release
  Design
} derive(Eq, Debug)

///|
pub(all) enum EvidenceStatus {
  Verified
  Pending
  Rejected
} derive(Eq, Debug)

///|
pub(all) enum Severity {
  Critical
  High
  Medium
  Low
  Info
} derive(Eq, Debug)

///|
pub(all) enum TraceIssueKind {
  MalformedLine
  UnknownRecord
  MissingField
  InvalidLevel
  InvalidEvidenceKind
  InvalidEvidenceStatus
} derive(Eq, Debug)

///|
pub(all) struct Capability {
  id : String
  title : String
  inputs : Array[String]
  outputs : Array[String]
  constraints : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct Requirement {
  id : String
  level : RequirementLevel
  text : String
  capabilities : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct Evidence {
  id : String
  kind : EvidenceKind
  target : String
  status : EvidenceStatus
  requirements : Array[String]
  note : String
} derive(Eq, Debug)

///|
pub(all) struct TraceIssue {
  kind : TraceIssueKind
  line : Int
  record : String
  message : String
} derive(Eq, Debug)

///|
pub(all) struct BoundarySpec {
  project : String
  purpose : String
  scopes : Array[String]
  out_of_scope : Array[String]
  capabilities : Array[Capability]
  requirements : Array[Requirement]
  evidence : Array[Evidence]
  issues : Array[TraceIssue]
} derive(Eq, Debug)

///|
pub(all) struct TraceFinding {
  id : String
  severity : Severity
  subject : String
  message : String
  remediation : String
} derive(Eq, Debug)

///|
pub(all) struct TraceReport {
  score : Int
  grade : String
  ready : Bool
  findings : Array[TraceFinding]
  spec : BoundarySpec
} derive(Eq, Debug)

///|
pub fn RequirementLevel::label(self : RequirementLevel) -> String {
  match self {
    Must => "must"
    Should => "should"
    May => "may"
  }
}

///|
pub fn EvidenceKind::label(self : EvidenceKind) -> String {
  match self {
    Test => "test"
    Example => "example"
    CI => "ci"
    Doc => "doc"
    Release => "release"
    Design => "design"
  }
}

///|
pub fn EvidenceStatus::label(self : EvidenceStatus) -> String {
  match self {
    Verified => "verified"
    Pending => "pending"
    Rejected => "rejected"
  }
}

///|
pub fn Severity::label(self : Severity) -> String {
  match self {
    Critical => "critical"
    High => "high"
    Medium => "medium"
    Low => "low"
    Info => "info"
  }
}

///|
pub fn Severity::weight(self : Severity) -> Int {
  match self {
    Critical => 35
    High => 20
    Medium => 10
    Low => 4
    Info => 0
  }
}

///|
pub fn TraceIssueKind::label(self : TraceIssueKind) -> String {
  match self {
    MalformedLine => "malformed-line"
    UnknownRecord => "unknown-record"
    MissingField => "missing-field"
    InvalidLevel => "invalid-level"
    InvalidEvidenceKind => "invalid-evidence-kind"
    InvalidEvidenceStatus => "invalid-evidence-status"
  }
}

///|
pub fn parse_level(value : StringView) -> RequirementLevel? {
  let normalized = value.trim().to_lower().to_owned()
  if normalized == "must" || normalized == "required" || normalized == "shall" {
    Some(Must)
  } else if normalized == "should" || normalized == "recommended" {
    Some(Should)
  } else if normalized == "may" || normalized == "optional" {
    Some(May)
  } else {
    None
  }
}

///|
pub fn parse_evidence_kind(value : StringView) -> EvidenceKind? {
  let normalized = value.trim().to_lower().to_owned()
  if normalized == "test" || normalized == "tests" {
    Some(Test)
  } else if normalized == "example" ||
    normalized == "demo" ||
    normalized == "smoke" {
    Some(Example)
  } else if normalized == "ci" || normalized == "workflow" {
    Some(CI)
  } else if normalized == "doc" ||
    normalized == "docs" ||
    normalized == "readme" {
    Some(Doc)
  } else if normalized == "release" || normalized == "mooncakes" {
    Some(Release)
  } else if normalized == "design" || normalized == "adr" {
    Some(Design)
  } else {
    None
  }
}

///|
pub fn parse_evidence_status(value : StringView) -> EvidenceStatus? {
  let normalized = value.trim().to_lower().to_owned()
  if normalized == "verified" || normalized == "pass" || normalized == "passed" {
    Some(Verified)
  } else if normalized == "pending" || normalized == "todo" {
    Some(Pending)
  } else if normalized == "rejected" ||
    normalized == "fail" ||
    normalized == "failed" {
    Some(Rejected)
  } else {
    None
  }
}

///|
pub fn parse_manifest(raw : StringView) -> BoundarySpec {
  let mut project = ""
  let mut purpose = ""
  let scopes : Array[String] = []
  let out_of_scope : Array[String] = []
  let capabilities : Array[Capability] = []
  let requirements : Array[Requirement] = []
  let evidence : Array[Evidence] = []
  let issues : Array[TraceIssue] = []
  for line_number, raw_line in raw.split("\n").iter2() {
    let line = raw_line.trim()
    if !line.is_empty() && !line.has_prefix("#") {
      match line.split_once(":") {
        Some((record_view, value_view)) => {
          let record = record_view.trim().to_lower().to_owned()
          let value = value_view.trim()
          match record {
            "project" => project = value.to_owned()
            "purpose" => purpose = value.to_owned()
            "scope" => push_non_empty(scopes, value)
            "out-of-scope" => push_non_empty(out_of_scope, value)
            "capability" =>
              match parse_capability(value, line_number + 1, issues) {
                Some(capability) => capabilities.push(capability)
                None => ()
              }
            "requirement" =>
              match parse_requirement(value, line_number + 1, issues) {
                Some(requirement) => requirements.push(requirement)
                None => ()
              }
            "evidence" =>
              match parse_evidence(value, line_number + 1, issues) {
                Some(item) => evidence.push(item)
                None => ()
              }
            _ =>
              issues.push({
                kind: UnknownRecord,
                line: line_number + 1,
                record,
                message: "record must be project, purpose, scope, out-of-scope, capability, requirement, or evidence",
              })
          }
        }
        None =>
          issues.push({
            kind: MalformedLine,
            line: line_number + 1,
            record: "",
            message: "manifest line must use ': '",
          })
      }
    }
  }
  {
    project,
    purpose,
    scopes,
    out_of_scope,
    capabilities,
    requirements,
    evidence,
    issues,
  }
}

///|
pub fn validate_manifest(raw : StringView) -> TraceReport {
  validate_spec(parse_manifest(raw))
}

///|
pub fn validate_spec(spec : BoundarySpec) -> TraceReport {
  let findings : Array[TraceFinding] = []
  for issue in spec.issues {
    findings.push({
      id: "parse." + issue.kind.label(),
      severity: High,
      subject: "line " + issue.line.to_string(),
      message: issue.message,
      remediation: "Fix the manifest record before publishing or using it in CI.",
    })
  }
  audit_summary_fields(spec, findings)
  audit_capabilities(spec, findings)
  audit_requirements(spec, findings)
  audit_evidence(spec, findings)
  let score = score_findings(findings)
  {
    score,
    grade: grade_score(score),
    ready: report_is_ready(findings),
    findings,
    spec,
  }
}

///|
pub fn BoundarySpec::capability(
  self : BoundarySpec,
  id : StringView,
) -> Capability? {
  let expected = id.trim().to_owned()
  self.capabilities.iter().find_first(capability => capability.id == expected)
}

///|
pub fn BoundarySpec::requirement(
  self : BoundarySpec,
  id : StringView,
) -> Requirement? {
  let expected = id.trim().to_owned()
  self.requirements.iter().find_first(requirement => requirement.id == expected)
}

///|
pub fn BoundarySpec::evidence_item(
  self : BoundarySpec,
  id : StringView,
) -> Evidence? {
  let expected = id.trim().to_owned()
  self.evidence.iter().find_first(item => item.id == expected)
}

///|
pub fn BoundarySpec::requirements_for_capability(
  self : BoundarySpec,
  capability_id : StringView,
) -> Array[Requirement] {
  let expected = capability_id.trim().to_owned()
  self.requirements.filter(requirement => {
    requirement.capabilities.any(id => id == expected)
  })
}

///|
pub fn BoundarySpec::evidence_for_requirement(
  self : BoundarySpec,
  requirement_id : StringView,
) -> Array[Evidence] {
  let expected = requirement_id.trim().to_owned()
  self.evidence.filter(item => item.requirements.any(id => id == expected))
}

///|
pub fn TraceReport::count_by_severity(
  self : TraceReport,
  severity : Severity,
) -> Int {
  self.findings.count_if(finding => finding.severity == severity)
}

///|
pub fn TraceReport::has_finding(self : TraceReport, id : StringView) -> Bool {
  let expected = id.to_owned()
  self.findings.any(finding => finding.id == expected)
}

///|
pub fn TraceReport::covered_requirements(self : TraceReport) -> Int {
  self.spec.requirements.count_if(requirement => {
    has_verified_evidence_for(self.spec, requirement.id)
  })
}

///|
pub fn TraceReport::coverage_percent(self : TraceReport) -> Int {
  if self.spec.requirements.is_empty() {
    0
  } else {
    self.covered_requirements() * 100 / self.spec.requirements.length()
  }
}

///|
pub fn TraceReport::to_markdown(self : TraceReport) -> String {
  let lines : Array[String] = []
  lines.push("# CapsuleTrace Report")
  lines.push("")
  lines.push("- Project: " + printable(self.spec.project))
  lines.push("- Score: " + self.score.to_string())
  lines.push("- Grade: " + self.grade)
  lines.push("- Ready: " + bool_label(self.ready))
  lines.push(
    "- Requirements covered: " +
    self.covered_requirements().to_string() +
    "/" +
    self.spec.requirements.length().to_string(),
  )
  lines.push("- Coverage: " + self.coverage_percent().to_string() + "%")
  lines.push("")
  lines.push("## Capability Boundary")
  lines.push("")
  lines.push("| Capability | Inputs | Outputs | Constraints |")
  lines.push("| --- | --- | --- | --- |")
  for capability in self.spec.capabilities {
    lines.push(
      "| " +
      markdown_cell(capability.id + " - " + capability.title) +
      " | " +
      markdown_cell(capability.inputs.join(", ")) +
      " | " +
      markdown_cell(capability.outputs.join(", ")) +
      " | " +
      markdown_cell(capability.constraints.join(", ")) +
      " |",
    )
  }
  lines.push("")
  lines.push("## Findings")
  lines.push("")
  lines.push("| Severity | Subject | Message |")
  lines.push("| --- | --- | --- |")
  if self.findings.is_empty() {
    lines.push("| info | all | No blocking findings. |")
  } else {
    for finding in self.findings {
      lines.push(
        "| " +
        finding.severity.label() +
        " | " +
        markdown_cell(finding.subject) +
        " | " +
        markdown_cell(finding.message) +
        " |",
      )
    }
  }
  lines.join("\n")
}

///|
pub fn TraceReport::to_json_string(self : TraceReport) -> String {
  Json::object(
    Map([
      (
        "score",
        Json::number(self.score.to_double(), repr=self.score.to_string()),
      ),
      ("grade", Json::string(self.grade)),
      ("ready", Json::boolean(self.ready)),
      (
        "coverage_percent",
        Json::number(
          self.coverage_percent().to_double(),
          repr=self.coverage_percent().to_string(),
        ),
      ),
      ("project", Json::string(self.spec.project)),
      ("purpose", Json::string(self.spec.purpose)),
      ("scopes", strings_to_json(self.spec.scopes)),
      ("out_of_scope", strings_to_json(self.spec.out_of_scope)),
      ("capabilities", capabilities_to_json(self.spec.capabilities)),
      ("requirements", requirements_to_json(self.spec.requirements)),
      ("evidence", evidence_to_json(self.spec.evidence)),
      ("findings", findings_to_json(self.findings)),
    ]),
  ).stringify(indent=2)
}

///|
pub fn sample_manifest() -> String {
  [
    "# CapsuleTrace line manifest", "project: CapsuleTrace", "purpose: Validate capability boundaries, test evidence, CI evidence, docs, and release readiness for MoonBit packages.",
    "scope: Parse a compact line manifest with no external dependencies.", "scope: Validate core acceptance evidence for library, example, CI, and release work.",
    "out-of-scope: Execute shell commands or contact package registries at runtime.",
    "capability: parse-manifest | Parse CapsuleTrace line records | text manifest | BoundarySpec | deterministic, offline",
    "capability: validate-trace | Validate requirements and evidence references | BoundarySpec | TraceReport | no network, no filesystem access",
    "capability: export-report | Export reviewer-friendly reports | TraceReport | Markdown and JSON strings | stable order",
    "requirement: R1 | must | Parser accepts project, scope, capability, requirement, and evidence records. | parse-manifest",
    "requirement: R2 | must | Validator reports missing evidence and broken references. | validate-trace",
    "requirement: R3 | should | Report exporter produces Markdown and JSON for CI artifacts. | export-report",
    "requirement: R4 | may | Manifest can document future maintenance boundaries. | validate-trace",
    "evidence: T1 | test | capsuletrace_wbtest.mbt | verified | R1,R2,R3 | Unit tests cover parser, validation, boundaries, and exporters.",
    "evidence: E1 | example | cmd/main | verified | R1,R3 | Runnable smoke example prints a markdown acceptance report.",
    "evidence: C1 | ci | .github/workflows/ci.yml | verified | R1,R2,R3 | GitHub Actions runs check, build, test, and example.",
    "evidence: D1 | doc | README.md | verified | R4 | Documentation defines supported and unsupported scope.",
  ].join("\n")
}

///|
pub fn incomplete_manifest() -> String {
  [
    "project: Boundary Draft", "purpose: Demonstrate validation failures.", "scope: Document a boundary without enough evidence.",
    "capability: parser | Parse records | manifest | spec | offline", "requirement: R1 | must | Parser must reject broken lines. | parser",
    "requirement: R2 | must | Validator must notice missing evidence. | missing-capability",
    "evidence: P1 | test | capsuletrace_wbtest.mbt | pending | R1 | Not executed yet.",
  ].join("\n")
}

///|
pub fn manifest_template(project : String, purpose : String) -> String {
  [
    "project: " + project,
    "purpose: " + purpose,
    "scope: ",
    "out-of-scope: ",
    "capability: cap-id | Capability title | inputs | outputs | constraints",
    "requirement: R1 | must | Requirement text. | cap-id",
    "evidence: T1 | test | test-file.mbt | verified | R1 | What the test proves.",
  ].join("\n")
}

///|
fn parse_capability(
  value : StringView,
  line : Int,
  issues : Array[TraceIssue],
) -> Capability? {
  let fields = split_fields(value)
  if fields.length() < 5 {
    issues.push(
      missing_field(
        line, "capability", "expected: id | title | inputs | outputs | constraints",
      ),
    )
    None
  } else {
    Some({
      id: fields[0],
      title: fields[1],
      inputs: split_csv(fields[2]),
      outputs: split_csv(fields[3]),
      constraints: split_csv(fields[4]),
    })
  }
}

///|
fn parse_requirement(
  value : StringView,
  line : Int,
  issues : Array[TraceIssue],
) -> Requirement? {
  let fields = split_fields(value)
  if fields.length() < 4 {
    issues.push(
      missing_field(
        line, "requirement", "expected: id | level | text | capability ids",
      ),
    )
    None
  } else {
    match parse_level(fields[1]) {
      Some(level) =>
        Some({
          id: fields[0],
          level,
          text: fields[2],
          capabilities: split_csv(fields[3]),
        })
      None => {
        issues.push({
          kind: InvalidLevel,
          line,
          record: "requirement",
          message: "requirement level must be must, should, or may",
        })
        None
      }
    }
  }
}

///|
fn parse_evidence(
  value : StringView,
  line : Int,
  issues : Array[TraceIssue],
) -> Evidence? {
  let fields = split_fields(value)
  if fields.length() < 6 {
    issues.push(
      missing_field(
        line, "evidence", "expected: id | kind | target | status | requirement ids | note",
      ),
    )
    None
  } else {
    match (parse_evidence_kind(fields[1]), parse_evidence_status(fields[3])) {
      (Some(kind), Some(status)) =>
        Some({
          id: fields[0],
          kind,
          target: fields[2],
          status,
          requirements: split_csv(fields[4]),
          note: fields[5],
        })
      (None, _) => {
        issues.push({
          kind: InvalidEvidenceKind,
          line,
          record: "evidence",
          message: "evidence kind must be test, example, ci, doc, release, or design",
        })
        None
      }
      (_, None) => {
        issues.push({
          kind: InvalidEvidenceStatus,
          line,
          record: "evidence",
          message: "evidence status must be verified, pending, or rejected",
        })
        None
      }
    }
  }
}

///|
fn audit_summary_fields(
  spec : BoundarySpec,
  findings : Array[TraceFinding],
) -> Unit {
  if spec.project.trim().is_empty() {
    findings.push({
      id: "summary.project-missing",
      severity: Critical,
      subject: "project",
      message: "project name is required",
      remediation: "Add 'project: ' to the manifest.",
    })
  }
  if spec.purpose.trim().is_empty() {
    findings.push({
      id: "summary.purpose-missing",
      severity: High,
      subject: "purpose",
      message: "project purpose is required",
      remediation: "Add 'purpose: ' to explain the package boundary.",
    })
  }
  if spec.scopes.is_empty() {
    findings.push({
      id: "summary.scope-missing",
      severity: High,
      subject: "scope",
      message: "at least one supported scope is required",
      remediation: "Add one or more 'scope:' records.",
    })
  }
  if spec.out_of_scope.is_empty() {
    findings.push({
      id: "summary.out-of-scope-missing",
      severity: Medium,
      subject: "out-of-scope",
      message: "explicit non-goals make the functional boundary reviewable",
      remediation: "Add one or more 'out-of-scope:' records.",
    })
  }
}

///|
fn audit_capabilities(
  spec : BoundarySpec,
  findings : Array[TraceFinding],
) -> Unit {
  if spec.capabilities.is_empty() {
    findings.push({
      id: "capability.none",
      severity: Critical,
      subject: "capability",
      message: "at least one capability is required",
      remediation: "Add 'capability:' records for the main reusable features.",
    })
  }
  let seen : Map[String, Bool] = Map([])
  for capability in spec.capabilities {
    audit_id("capability", capability.id, findings)
    if seen.contains(capability.id) {
      findings.push(duplicate_finding("capability", capability.id))
    } else {
      seen.set(capability.id, true)
    }
    if capability.title.trim().is_empty() {
      findings.push(
        field_finding(
          "capability.title-empty",
          High,
          capability.id,
          "capability title is required",
        ),
      )
    }
    if capability.inputs.is_empty() {
      findings.push(
        field_finding(
          "capability.inputs-empty",
          High,
          capability.id,
          "capability inputs are required",
        ),
      )
    }
    if capability.outputs.is_empty() {
      findings.push(
        field_finding(
          "capability.outputs-empty",
          High,
          capability.id,
          "capability outputs are required",
        ),
      )
    }
    if capability.constraints.is_empty() {
      findings.push(
        field_finding(
          "capability.constraints-empty",
          Medium,
          capability.id,
          "capability constraints document the boundary",
        ),
      )
    }
    if spec.requirements_for_capability(capability.id).is_empty() {
      findings.push({
        id: "capability.no-requirement",
        severity: Medium,
        subject: capability.id,
        message: "capability is not covered by a requirement",
        remediation: "Add at least one requirement that references this capability.",
      })
    }
  }
}

///|
fn audit_requirements(
  spec : BoundarySpec,
  findings : Array[TraceFinding],
) -> Unit {
  if spec.requirements.is_empty() {
    findings.push({
      id: "requirement.none",
      severity: Critical,
      subject: "requirement",
      message: "at least one requirement is needed to trace evidence",
      remediation: "Add 'requirement:' records for observable behavior.",
    })
  }
  let seen : Map[String, Bool] = Map([])
  for requirement in spec.requirements {
    audit_id("requirement", requirement.id, findings)
    if seen.contains(requirement.id) {
      findings.push(duplicate_finding("requirement", requirement.id))
    } else {
      seen.set(requirement.id, true)
    }
    if requirement.text.trim().is_empty() {
      findings.push(
        field_finding(
          "requirement.text-empty",
          High,
          requirement.id,
          "requirement text is required",
        ),
      )
    }
    if requirement.capabilities.is_empty() {
      findings.push(
        field_finding(
          "requirement.capability-empty",
          High,
          requirement.id,
          "requirement must reference a capability",
        ),
      )
    }
    for capability_id in requirement.capabilities {
      if spec.capability(capability_id) is None {
        findings.push({
          id: "requirement.unknown-capability",
          severity: High,
          subject: requirement.id,
          message: "requirement references unknown capability '" +
          capability_id +
          "'",
          remediation: "Declare the capability or fix the requirement reference.",
        })
      }
    }
    match requirement.level {
      Must =>
        if !has_verified_evidence_for(spec, requirement.id) {
          findings.push({
            id: "requirement.must-unverified",
            severity: High,
            subject: requirement.id,
            message: "must requirement has no verified evidence",
            remediation: "Attach verified test, example, CI, doc, or release evidence.",
          })
        }
      Should =>
        if spec.evidence_for_requirement(requirement.id).is_empty() {
          findings.push({
            id: "requirement.should-uncovered",
            severity: Medium,
            subject: requirement.id,
            message: "should requirement has no evidence",
            remediation: "Attach evidence or lower the requirement level.",
          })
        }
      May => ()
    }
  }
}

///|
fn audit_evidence(spec : BoundarySpec, findings : Array[TraceFinding]) -> Unit {
  if spec.evidence.is_empty() {
    findings.push({
      id: "evidence.none",
      severity: Critical,
      subject: "evidence",
      message: "at least one evidence record is required",
      remediation: "Add evidence for tests, examples, documentation, CI, or release artifacts.",
    })
  }
  let seen : Map[String, Bool] = Map([])
  let mut has_test = false
  let mut has_example = false
  let mut has_ci = false
  for item in spec.evidence {
    audit_id("evidence", item.id, findings)
    if seen.contains(item.id) {
      findings.push(duplicate_finding("evidence", item.id))
    } else {
      seen.set(item.id, true)
    }
    if item.kind == Test && item.status == Verified {
      has_test = true
    }
    if item.kind == Example && item.status == Verified {
      has_example = true
    }
    if item.kind == CI && item.status == Verified {
      has_ci = true
    }
    if item.target.trim().is_empty() {
      findings.push(
        field_finding(
          "evidence.target-empty",
          High,
          item.id,
          "evidence target is required",
        ),
      )
    }
    if item.requirements.is_empty() {
      findings.push(
        field_finding(
          "evidence.requirement-empty",
          High,
          item.id,
          "evidence must reference at least one requirement",
        ),
      )
    }
    if item.status == Pending {
      findings.push({
        id: "evidence.pending",
        severity: Low,
        subject: item.id,
        message: "evidence is listed but not verified",
        remediation: "Run or review the evidence, then mark it verified.",
      })
    }
    if item.status == Rejected {
      findings.push({
        id: "evidence.rejected",
        severity: High,
        subject: item.id,
        message: "evidence is rejected",
        remediation: "Replace it with valid evidence or fix the failing artifact.",
      })
    }
    for requirement_id in item.requirements {
      if spec.requirement(requirement_id) is None {
        findings.push({
          id: "evidence.unknown-requirement",
          severity: High,
          subject: item.id,
          message: "evidence references unknown requirement '" +
          requirement_id +
          "'",
          remediation: "Declare the requirement or fix the evidence reference.",
        })
      }
    }
  }
  if !has_test {
    findings.push({
      id: "evidence.verified-test-missing",
      severity: High,
      subject: "test",
      message: "no verified test evidence is present",
      remediation: "Add a verified test evidence record.",
    })
  }
  if !has_example {
    findings.push({
      id: "evidence.verified-example-missing",
      severity: Medium,
      subject: "example",
      message: "no verified runnable example evidence is present",
      remediation: "Add a verified example evidence record.",
    })
  }
  if !has_ci {
    findings.push({
      id: "evidence.verified-ci-missing",
      severity: Medium,
      subject: "ci",
      message: "no verified CI evidence is present",
      remediation: "Add a verified CI evidence record.",
    })
  }
}

///|
fn audit_id(
  category : String,
  id : String,
  findings : Array[TraceFinding],
) -> Unit {
  if !looks_like_id(id) {
    findings.push({
      id: category + ".invalid-id",
      severity: High,
      subject: id,
      message: category +
      " id must be non-empty and cannot contain spaces, '|', or ':'",
      remediation: "Use lowercase identifiers like parser, R1, trace-export, or cli.smoke.",
    })
  }
}

///|
fn score_findings(findings : Array[TraceFinding]) -> Int {
  let mut score = 100
  for finding in findings {
    score = score - finding.severity.weight()
  }
  if score < 0 {
    0
  } else {
    score
  }
}

///|
fn grade_score(score : Int) -> String {
  if score >= 90 {
    "A"
  } else if score >= 75 {
    "B"
  } else if score >= 60 {
    "C"
  } else if score >= 40 {
    "D"
  } else {
    "F"
  }
}

///|
fn report_is_ready(findings : Array[TraceFinding]) -> Bool {
  !findings.any(finding => {
    finding.severity == Critical || finding.severity == High
  })
}

///|
fn has_verified_evidence_for(
  spec : BoundarySpec,
  requirement_id : String,
) -> Bool {
  spec.evidence.any(item => {
    item.status == Verified && item.requirements.any(id => id == requirement_id)
  })
}

///|
fn split_fields(value : StringView) -> Array[String] {
  let fields : Array[String] = []
  for field in value.split("|") {
    fields.push(field.trim().to_owned())
  }
  fields
}

///|
fn split_csv(value : StringView) -> Array[String] {
  let items : Array[String] = []
  for raw_item in value.split(",") {
    let item = raw_item.trim()
    if !item.is_empty() {
      items.push(item.to_owned())
    }
  }
  items
}

///|
fn push_non_empty(items : Array[String], value : StringView) -> Unit {
  let trimmed = value.trim()
  if !trimmed.is_empty() {
    items.push(trimmed.to_owned())
  }
}

///|
fn looks_like_id(id : String) -> Bool {
  let value = id.trim().to_owned()
  !value.is_empty() &&
  !value.contains(" ") &&
  !value.contains("|") &&
  !value.contains(":")
}

///|
fn missing_field(line : Int, record : String, message : String) -> TraceIssue {
  { kind: MissingField, line, record, message }
}

///|
fn duplicate_finding(category : String, id : String) -> TraceFinding {
  {
    id: category + ".duplicate-id",
    severity: High,
    subject: id,
    message: category + " id appears more than once",
    remediation: "Keep identifiers unique so trace links are deterministic.",
  }
}

///|
fn field_finding(
  id : String,
  severity : Severity,
  subject : String,
  message : String,
) -> TraceFinding {
  {
    id,
    severity,
    subject,
    message,
    remediation: "Fill the missing field in the manifest record.",
  }
}

///|
fn printable(value : String) -> String {
  if value.trim().is_empty() {
    ""
  } else {
    value
  }
}

///|
fn bool_label(value : Bool) -> String {
  if value {
    "true"
  } else {
    "false"
  }
}

///|
fn markdown_cell(value : String) -> String {
  value.replace_all(old="|", new="\\|").replace_all(old="\n", new=" ")
}

///|
fn strings_to_json(items : Array[String]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(Json::string(item))
  }
  Json::array(values)
}

///|
fn capabilities_to_json(items : Array[Capability]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("id", Json::string(item.id)),
          ("title", Json::string(item.title)),
          ("inputs", strings_to_json(item.inputs)),
          ("outputs", strings_to_json(item.outputs)),
          ("constraints", strings_to_json(item.constraints)),
        ]),
      ),
    )
  }
  Json::array(values)
}

///|
fn requirements_to_json(items : Array[Requirement]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("id", Json::string(item.id)),
          ("level", Json::string(item.level.label())),
          ("text", Json::string(item.text)),
          ("capabilities", strings_to_json(item.capabilities)),
        ]),
      ),
    )
  }
  Json::array(values)
}

///|
fn evidence_to_json(items : Array[Evidence]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("id", Json::string(item.id)),
          ("kind", Json::string(item.kind.label())),
          ("target", Json::string(item.target)),
          ("status", Json::string(item.status.label())),
          ("requirements", strings_to_json(item.requirements)),
          ("note", Json::string(item.note)),
        ]),
      ),
    )
  }
  Json::array(values)
}

///|
fn findings_to_json(items : Array[TraceFinding]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("id", Json::string(item.id)),
          ("severity", Json::string(item.severity.label())),
          ("subject", Json::string(item.subject)),
          ("message", Json::string(item.message)),
          ("remediation", Json::string(item.remediation)),
        ]),
      ),
    )
  }
  Json::array(values)
}