///|
/// 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)
}