use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::time::Instant;
use crate::ontology::arming::ArmedShapes;
use crate::ontology::capability_cells;
use crate::ontology::extract::release_inputs::Subject;
use crate::ontology::extract::{
self, apr_model, binary, cli_surface, code, csv, example, ggplot2_core, gguf, json, kernel,
lean, llm_context, parity_receipt, pv_contract, readme, release_evidence, ExtractFailure,
};
use crate::ontology::measured_sets;
use crate::ontology::rdf::{iri, Graph, Term, RDF_TYPE};
use crate::ontology::receipts;
use crate::ontology::shapes::{self, NodeShape, Report, Severity, ShapeError};
use crate::ontology::verdict::Reason;
use crate::ontology::w3c;
use super::capability_cells_gate as cells_gate;
use super::finding::LintFinding;
use super::ggplot2_core_gate as ggplot2_gate;
use super::rules::RuleSeverity;
use super::{GateDetail, GateExtra, GateResult, Verdict};
pub const PLANT_ID: &str = "__pc_shape__";
#[derive(Debug)]
pub enum ShapesOutcome {
Unsupported(ShapeError),
ExtractFailed(ExtractFailure),
NoShapes { contracts_checked: usize },
NoFocus { shapes_n: usize },
NoReceipts { shapes_n: usize, dir: String },
WrongCorpus {
shapes_n: usize,
expected: usize,
found: usize,
refused: Vec<String>,
},
EmptyDomain { shapes_n: usize },
PositiveControlFailed {
shapes_n: usize,
focus_nodes_n: usize,
which: String,
},
Differential {
shapes_n: usize,
focus_nodes_n: usize,
passed: usize,
n: usize,
failed: Vec<String>,
},
HarnessBroken { causes: Vec<String> },
Ran {
result: Box<GateResult>,
findings: Vec<LintFinding>,
},
}
#[must_use]
pub fn target_class_map(contract_dir: &Path) -> shapes::TargetMap {
let Ok(text) = std::fs::read_to_string(contract_dir.join("ontology.yaml")) else {
return shapes::TargetMap::new();
};
let Ok(doc) = serde_yaml::from_str::<serde_yaml::Value>(&text) else {
return shapes::TargetMap::new();
};
doc.get("entity_type_target_class")
.and_then(serde_yaml::Value::as_mapping)
.map(|m| {
m.iter()
.filter_map(|(k, v)| Some((k.as_str()?.to_string(), v.as_str()?.to_string())))
.collect()
})
.unwrap_or_default()
}
pub fn collect_shapes(
contract_dir: &Path,
) -> Result<(Vec<(NodeShape, String)>, usize), ShapeError> {
let mut shapes: Vec<(NodeShape, String)> = Vec::new();
let mut checked = 0usize;
let targets = target_class_map(contract_dir);
for (stem, rel, doc) in pv_contract::documents(contract_dir) {
checked += 1;
for shape in shapes::parse_shapes_with(&stem, &doc, &targets)? {
if let Some((_, other)) = shapes.iter().find(|(s, _)| s.id == shape.id) {
return Err(ShapeError::Malformed {
shape: shape.id.clone(),
what: format!("declared twice: in {other} and in {rel}"),
});
}
shapes.push((shape, rel.clone()));
}
}
Ok((shapes, checked))
}
pub fn declared_shapes(contract_dir: &Path) -> Result<Vec<(String, String)>, ShapeError> {
Ok(collect_shapes(contract_dir)?
.0
.into_iter()
.map(|(s, f)| (s.id, f))
.collect())
}
fn armed_shapes_of(contract_dir: &Path) -> Result<ArmedShapes, ShapeError> {
let text = std::fs::read_to_string(contract_dir.join("lint-baseline.json")).ok();
ArmedShapes::from_baseline(text.as_deref()).map_err(|e| ShapeError::Malformed {
shape: "lint-baseline.json".into(),
what: e.to_string(),
})
}
fn parity_refusal(
parity: &extract::parity_receipt::ParityStats,
shapes_n: usize,
) -> Option<ShapesOutcome> {
let refused = || parity.errors.iter().map(ToString::to_string).collect();
if let Some((expected, found)) = parity.wrong_corpus() {
return Some(ShapesOutcome::WrongCorpus {
shapes_n,
expected,
found,
refused: refused(),
});
}
if !parity.errors.is_empty() {
return Some(ShapesOutcome::WrongCorpus {
shapes_n,
expected: parity.expected.unwrap_or(parity.records),
found: parity.records,
refused: refused(),
});
}
None
}
fn carry_extract_warnings(report: &mut Report, warnings: &[extract::json::Warning]) {
for w in warnings {
report.results.push(shapes::ValidationResult {
severity: Severity::Warning,
focus: iri("json", &w.contract),
shape: w.contract.clone(),
path: Some(w.path.clone()),
component: "extract:json",
message: w.to_string(),
});
}
}
fn w3c_checked(shapes_n: usize, focus_nodes_n: usize) -> Result<w3c::W3cRun, ShapesOutcome> {
let w3c_run = w3c::run_all();
let failed = w3c_run.failed();
if failed.is_empty() {
return Ok(w3c_run);
}
Err(ShapesOutcome::Differential {
shapes_n,
focus_nodes_n,
passed: w3c_run.passed(),
n: w3c_run.results.len(),
failed,
})
}
#[derive(Debug, Clone, Default)]
pub struct ShapesOptions {
pub only: Option<String>,
pub release: Option<Subject>,
}
fn select_family(
declared: Vec<(NodeShape, String)>,
only: Option<&str>,
) -> Result<Vec<NodeShape>, ShapeError> {
let Some(id) = only else {
return Ok(declared.into_iter().map(|(s, _)| s).collect());
};
let prefix = format!("{id}.");
let family: Vec<NodeShape> = declared
.into_iter()
.map(|(s, _)| s)
.filter(|s| s.id == id || s.id.starts_with(&prefix))
.collect();
if family.iter().any(|s| s.id == id) {
Ok(family)
} else {
Err(ShapeError::Malformed {
shape: id.to_string(),
what: "--shape names no declared shape".into(),
})
}
}
#[must_use]
pub fn run_shapes_gate(contract_dir: &Path) -> ShapesOutcome {
run_shapes_gate_with(contract_dir, &ShapesOptions::default())
}
#[must_use]
pub fn run_shapes_gate_with(contract_dir: &Path, opts: &ShapesOptions) -> ShapesOutcome {
match run_or_answer(contract_dir, opts) {
Ok(ran) | Err(ran) => ran,
}
}
fn run_or_answer(
contract_dir: &Path,
opts: &ShapesOptions,
) -> Result<ShapesOutcome, ShapesOutcome> {
let start = Instant::now();
let (shapes, arming, checked) = prepare(contract_dir, opts)?;
let extraction = extract_gradeable(contract_dir, opts, shapes.len())?;
let mut owned = extraction.graph.clone();
let cells = cells_corpus(&mut owned, &shapes, &extraction)?;
let graph = &owned;
let focus_of = focus_of(graph, &shapes);
let (vacuous_any, armed_vacuity) = vacuities(&focus_of, &shapes, &arming);
let (mut report, plant_violations) = validate_with_plant(graph, &shapes, &arming);
if opts.only.is_some() {
order_by_family(&mut report, &shapes);
}
carry_extract_warnings(&mut report, &extraction.warnings);
let pc_extract = extract_controls();
let pc_shapes = cells_controls(
&shapes,
&arming,
&report,
&extraction.gguf.rungs,
cells.as_ref(),
(&extraction.ggplot2, opts.only.is_some()),
)?;
let unmeasured = needs_receipts(&shapes) && extraction.receipts.is_empty();
let all_allow_empty = shapes.iter().all(|s| s.allow_empty.is_some());
if let Some(d) = decline(
shapes.len(),
&report,
all_allow_empty,
unmeasured,
plant_violations,
&pc_extract,
) {
return Err(d);
}
let w3c_run = w3c_checked(shapes.len(), report.focus_nodes_n)?;
let mut counted = findings_of(&report, &arming, graph, &extraction);
count_freshness(&mut counted, contract_dir, &extraction);
let (inherited_shapes_applied, inherited_by_shape) =
subsumption_of(contract_dir, graph, &shapes, &mut counted);
counted.findings.extend(
cells
.iter()
.flat_map(|cc| cells_gate::findings(cc, &arming)),
);
refuse_vacuous_kernels(&mut counted, &shapes, &arming, extraction.kernel.kernels);
refuse_unresolved_kernel_claims(
&mut counted,
extraction.kernel.kernels,
&kernel_claims(contract_dir, extraction.kernel.receipt_files),
);
let passed = counted.violations == 0;
let verdict = verdict_of(&counted, armed_vacuity);
let by_shape = by_shape(&focus_of);
let (armed_names, not_armed): (Vec<String>, Vec<String>) = shapes
.iter()
.map(|s| s.id.clone())
.filter(|id| !vacuous_any.contains(id))
.partition(|id| arming.is_armed(id));
let by_entity_type = by_entity_type(&extraction, &sigma_implemented(contract_dir))
.map_err(ShapesOutcome::Unsupported)?;
let duration = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
let result = GateResult {
name: "shapes".into(),
passed,
skipped: false,
verdict,
duration_ms: duration,
detail: GateDetail::Validate {
contracts: checked,
errors: counted.violations,
warnings: counted.warnings,
error_messages: counted
.findings
.iter()
.filter(|f| f.severity == RuleSeverity::Error)
.map(|f| f.message.clone())
.collect(),
},
extra: Some(GateExtra::Shapes {
shapes_n: shapes.len(),
focus_nodes_n: report.focus_nodes_n,
pc_shape: "fired".into(),
plant_violations,
violations: counted.violations,
warnings: counted.warnings,
by_shape,
triples: graph.len(),
armed_shapes: armed_names,
not_armed_shapes: not_armed,
declines: vacuous_any.clone(),
unarmed_violations: counted.unarmed_violations,
by_entity_type,
controls: Box::new(super::ShapesControls {
pc_extract,
pc_shapes,
capability_cells: cells.as_ref().map(cells_gate::CapabilityCellsReport::from),
ggplot2_core: ggplot2_gate::declared(&shapes)
.then(|| ggplot2_gate::Ggplot2CoreReport::from(&extraction.ggplot2)),
}),
receipts: extraction.resolve.receipts,
witnesses: extraction.resolve.witnesses,
hex_mismatches: extraction.resolve.hex_mismatches,
unmeasured_rows: extraction.resolve.unmeasured_rows,
w3c_cases_passed: w3c_run.passed(),
w3c_cases_n: w3c_run.results.len(),
counters: Box::new(super::ShapesCounters {
symbols_resolved: extraction.code.resolved,
symbols_unresolved: extraction.code.unresolved,
lean_statements: extraction.lean.statements,
lean_refs_unresolved: extraction.lean.refs_unresolved.len(),
inherited_shapes_applied,
inherited_by_shape,
}),
release: extraction.release.clone().map(Box::new),
readme: measured(&extraction.readme),
claude_md: measured(&extraction.llm_context),
ratchets: super::Ratchets::default(),
}),
};
Ok(ShapesOutcome::Ran {
result: Box::new(result),
findings: counted.findings,
})
}
fn extract_gradeable(
contract_dir: &Path,
opts: &ShapesOptions,
shapes_n: usize,
) -> Result<extract::Extraction, ShapesOutcome> {
let extraction = extract::all_with(contract_dir, opts.release.as_ref())
.map_err(ShapesOutcome::ExtractFailed)?;
if let Some(refusal) = parity_refusal(&extraction.parity, shapes_n) {
return Err(refusal);
}
match harness_broken(&extraction) {
Some(broken) => Err(broken),
None => Ok(extraction),
}
}
fn cells_corpus(
owned: &mut Graph,
shapes: &[NodeShape],
extraction: &extract::Extraction,
) -> Result<Option<capability_cells::CapabilityCells>, ShapesOutcome> {
let cells = cells_gate::corpus(owned, shapes, &extraction.gguf.rungs, &extraction.receipts)
.map_err(|e| {
ShapesOutcome::ExtractFailed(ExtractFailure::Receipt(receipts::ReceiptError {
file: e.file(),
what: e.to_string(),
}))
})?;
if cells.as_ref().is_some_and(|c| c.domain.is_empty()) {
return Err(ShapesOutcome::EmptyDomain {
shapes_n: shapes.len(),
});
}
Ok(cells)
}
fn cells_controls(
shapes: &[NodeShape],
arming: &ArmedShapes,
report: &Report,
rungs: &[extract::gguf::Rung],
cells: Option<&capability_cells::CapabilityCells>,
ggplot2: (&ggplot2_core::Ggplot2Stats, bool),
) -> Result<BTreeMap<String, String>, ShapesOutcome> {
let mut pc_shapes = cells_gate::pc_shapes(shapes, arming);
let (gg, scoped) = ggplot2;
if let Some(v) = ggplot2_gate::control(shapes, gg, scoped) {
pc_shapes.insert(ggplot2_gate::SHAPE.to_string(), v);
}
if let Some((which, _)) = pc_shapes.iter().find(|(_, v)| v.as_str() != "fired") {
return Err(ShapesOutcome::PositiveControlFailed {
shapes_n: shapes.len(),
focus_nodes_n: report.focus_nodes_n,
which: format!("pc_shapes.{which}"),
});
}
if let Some(cc) = cells {
if !cells_gate::wiring_holds(report, rungs, cc) {
return Err(ShapesOutcome::Differential {
shapes_n: shapes.len(),
focus_nodes_n: report.focus_nodes_n,
passed: 0,
n: 1,
failed: vec![format!(
"capability-cells wiring: the rungs the shape flagged differ from the rungs owning the {} NotRun cell(s)",
cc.not_run.len()
)],
});
}
}
if ggplot2_gate::declared(shapes) && !ggplot2_gate::wiring_holds(report, gg) {
return Err(ShapesOutcome::Differential {
shapes_n: shapes.len(),
focus_nodes_n: report.focus_nodes_n,
passed: 0,
n: 1,
failed: vec![format!(
"ggplot2-core wiring: the rows the shape flagged differ from the {} row(s) counted not proven",
gg.not_proven.len()
)],
});
}
Ok(pc_shapes)
}
fn prepare(
contract_dir: &Path,
opts: &ShapesOptions,
) -> Result<(Vec<NodeShape>, ArmedShapes, usize), ShapesOutcome> {
let (declared, checked) = collect_shapes(contract_dir).map_err(ShapesOutcome::Unsupported)?;
if declared.is_empty() {
return Err(ShapesOutcome::NoShapes {
contracts_checked: checked,
});
}
let baseline = armed_shapes_of(contract_dir).map_err(ShapesOutcome::Unsupported)?;
let arming = if opts.only.is_some() {
ArmedShapes::All
} else {
baseline
};
let shapes =
select_family(declared, opts.only.as_deref()).map_err(ShapesOutcome::Unsupported)?;
Ok((shapes, arming, checked))
}
fn order_by_family(report: &mut Report, shapes: &[NodeShape]) {
let rank = |id: &str| shapes.iter().position(|s| s.id == id).unwrap_or(usize::MAX);
report.results.sort_by_key(|r| rank(&r.shape));
}
fn needs_receipts(shapes: &[NodeShape]) -> bool {
shapes.iter().any(|s| {
s.properties
.iter()
.any(|p| p.resolves.as_deref() == Some("receipt"))
})
}
fn focus_of(graph: &Graph, shapes: &[NodeShape]) -> Vec<(String, usize)> {
shapes
.iter()
.map(|s| {
(
s.id.clone(),
shapes::instances_closed(graph, &s.target_class).len(),
)
})
.collect()
}
fn vacuities(
focus_of: &[(String, usize)],
shapes: &[NodeShape],
arming: &ArmedShapes,
) -> (Vec<String>, bool) {
let vacuous: Vec<String> = focus_of
.iter()
.filter(|(_, n)| *n == 0)
.map(|(id, _)| id.clone())
.collect();
let may_be_empty = |id: &str| shapes.iter().any(|s| s.id == id && s.allow_empty.is_some());
let armed = vacuous
.iter()
.any(|id| arming.is_armed(id) && !may_be_empty(id));
(vacuous, armed)
}
fn verdict_of(counted: &Counted, armed_vacuity: bool) -> Verdict {
if counted.violations > 0 {
Verdict::Fail
} else if armed_vacuity {
Verdict::Unknown(Reason::NoFocus)
} else if counted.warnings > 0 {
Verdict::Unknown(Reason::Warn)
} else {
Verdict::Pass
}
}
fn by_shape(focus_of: &[(String, usize)]) -> Vec<String> {
let mut out: Vec<String> = focus_of.iter().map(|(id, n)| format!("{id}={n}")).collect();
out.sort();
out
}
const COUNTED_ENTITY_TYPES: &[&str] = &[
"pv-contract",
"json",
"gguf",
"apr-model",
"parity-receipt",
"code",
"lean",
"example",
"binary",
"release-evidence",
"cli-surface",
"readme",
"llm-context",
"csv",
"kernel",
"ggplot2-core",
"ggplot2-row",
];
fn entity_count(name: &str, extraction: &extract::Extraction) -> Option<usize> {
Some(match name {
"pv-contract" => extraction
.graph
.instances_of(&crate::ontology::rdf::ont("Contract"))
.len(),
"json" => extraction.entities_extracted.len(),
"gguf" => extraction.gguf.rungs.len() + extraction.gguf.files_read,
"apr-model" => extraction.apr_model.files_read,
"parity-receipt" => extraction.parity.records,
"code" => extraction.code.symbols,
"lean" => extraction.lean.statements,
"example" => extraction.example.examples,
"binary" => extraction.binary.targets,
_ => return entity_count_tail(name, extraction),
})
}
fn entity_count_tail(name: &str, extraction: &extract::Extraction) -> Option<usize> {
Some(match name {
"release-evidence" => extraction.release.as_ref().map_or(0, |r| r.cells),
"cli-surface" => extraction
.release
.as_ref()
.and_then(|r| r.surface.as_ref())
.map_or(0, |s| s.leaves),
"readme" => extraction.readme.files_read,
"llm-context" => extraction.llm_context.files_read,
"csv" => extraction.csv.files_read,
"kernel" => extraction.kernel.kernels,
"ggplot2-core" => extraction.ggplot2.domain.len(),
"ggplot2-row" => extraction.ggplot2.bound,
_ => return extraction.github.by_type.get(name).copied(),
})
}
fn sigma_implemented(contract_dir: &Path) -> BTreeMap<String, String> {
std::fs::read_to_string(contract_dir.join("ontology.yaml"))
.ok()
.and_then(|text| crate::ontology::sigma::Sigma::from_yaml(&text).ok())
.map(|sigma| {
sigma
.entity_types
.into_iter()
.filter(|e| e.implemented)
.map(|e| (e.name, e.extractor))
.collect()
})
.unwrap_or_default()
}
fn by_entity_type(
extraction: &extract::Extraction,
implemented: &BTreeMap<String, String>,
) -> Result<BTreeMap<String, usize>, ShapeError> {
COUNTED_ENTITY_TYPES
.iter()
.map(|s| (*s).to_string())
.chain(extraction.github.by_type.keys().cloned())
.chain(implemented.keys().cloned())
.map(|name| {
let count = entity_count(&name, extraction).or_else(|| {
(implemented.get(&name).map(String::as_str) == Some("pv_contract")).then(|| {
extraction
.graph
.instances_of(&extract::pv_contract::entity_class(&name))
.len()
})
});
(name, count)
})
.map(|(name, count)| match count {
Some(n) => Ok((name, n)),
None => Err(ShapeError::Malformed {
shape: "by_entity_type".into(),
what: format!(
"entity type {name} is registered in Σ as implemented and is not counted in by_entity_type"
),
}),
})
.collect()
}
fn harness_broken(extraction: &extract::Extraction) -> Option<ShapesOutcome> {
let causes = extraction
.release
.as_ref()?
.crux
.as_ref()?
.harness_broken
.clone();
(!causes.is_empty()).then_some(ShapesOutcome::HarnessBroken { causes })
}
fn decline(
shapes_n: usize,
report: &Report,
all_allow_empty: bool,
unmeasured: bool,
plant_violations: usize,
pc_extract: &BTreeMap<String, String>,
) -> Option<ShapesOutcome> {
if report.focus_nodes_n == 0 && !all_allow_empty {
return Some(ShapesOutcome::NoFocus { shapes_n });
}
if unmeasured {
return Some(ShapesOutcome::NoReceipts {
shapes_n,
dir: receipts::EVIDENCE_DIR.to_string(),
});
}
let silent = if plant_violations == 0 {
Some("pc_shape".to_string())
} else {
pc_extract
.iter()
.find(|(_, v)| v.as_str() != "fired")
.map(|(k, _)| format!("pc_extract.{k}"))
};
silent.map(|which| ShapesOutcome::PositiveControlFailed {
shapes_n,
focus_nodes_n: report.focus_nodes_n,
which,
})
}
fn extract_controls() -> BTreeMap<String, String> {
let apr_sample = apr_model::minimal_container(2);
[
("pv-contract", pv_contract::positive_control()),
("json", json::positive_control()),
("gguf", gguf::positive_control()),
("apr-model", apr_model::positive_control(&apr_sample)),
("code", code::positive_control()),
("lean", lean::positive_control()),
("example", example::positive_control()),
(
"binary",
binary::positive_control(&binary::control_sample()),
),
("kernel", kernel::positive_control()),
(
"parity-receipt",
parity_receipt::positive_control(&parity_receipt::control_sample()),
),
("release-evidence", release_evidence::positive_control()),
("repo", json::github::positive_control("repo")),
("issue", json::github::positive_control("issue")),
(
"pull-request",
json::github::positive_control("pull-request"),
),
("milestone", json::github::positive_control("milestone")),
("readme", readme::positive_control()),
("llm-context", llm_context::positive_control()),
("csv", csv::positive_control()),
("ggplot2-core", ggplot2_core::positive_control()),
("ggplot2-row", ggplot2_core::positive_control()),
("cli-surface", cli_surface::positive_control()),
]
.into_iter()
.map(|(k, fired)| {
(
k.to_string(),
if fired { "fired" } else { "not-fired" }.to_string(),
)
})
.collect()
}
fn measured(stats: &extract::claims::DocStats) -> super::MeasuredSet {
super::MeasuredSet {
verified_commands: stats.verified_commands.iter().cloned().collect(),
}
}
fn count_freshness(c: &mut Counted, contract_dir: &Path, extraction: &extract::Extraction) {
let path = contract_dir.join("lint-baseline.json");
let text = std::fs::read_to_string(&path).ok();
let live = [
(measured_sets::KEYS[0], &extraction.readme.verified_commands),
(
measured_sets::KEYS[1],
&extraction.llm_context.verified_commands,
),
];
for (key, live) in live {
let messages = match measured_sets::read(text.as_deref(), key) {
Ok(committed) => measured_sets::freshness(key, &committed, live),
Err(e) => vec![format!("F-33: {e}")],
};
for m in messages {
c.violations += 1;
let mut f = LintFinding::new(
"PV-ONT-013",
RuleSeverity::Error,
m,
path.display().to_string(),
);
f.contract_stem = None;
c.findings.push(f);
}
}
}
struct Counted {
findings: Vec<LintFinding>,
violations: usize,
warnings: usize,
unarmed_violations: usize,
}
fn refuse_vacuous_kernels(
c: &mut Counted,
shapes: &[NodeShape],
arming: &ArmedShapes,
kernels: usize,
) {
let kernel_class = crate::ontology::rdf::ont("KernelSymbol");
let (armed, unarmed): (Vec<&str>, Vec<&str>) = shapes
.iter()
.filter(|s| s.target_class == kernel_class)
.map(|s| s.id.as_str())
.partition(|id| arming.is_armed(id));
if kernels > 0 {
return;
}
if !armed.is_empty() {
c.violations += 1;
c.findings
.push(vacuous_kernel_finding(&armed, RuleSeverity::Error, ""));
}
if !unarmed.is_empty() {
c.unarmed_violations += 1;
c.findings.push(vacuous_kernel_finding(
&unarmed,
RuleSeverity::Warning,
" [not armed]",
));
}
}
fn kernel_claims(contract_dir: &Path, receipt_files: usize) -> Vec<String> {
let mut out: Vec<String> = extract::code::registries(contract_dir)
.iter()
.flat_map(|(_, r)| r.bindings.iter().filter(|b| b.kernel))
.map(|b| {
format!(
"binding {}::{}",
b.module_path.as_deref().unwrap_or_default(),
b.function.as_deref().unwrap_or_default()
)
})
.collect();
if receipt_files > 0 {
out.push(format!(
"{receipt_files} receipt file(s) under {}",
kernel::EVIDENCE_DIR
));
}
out
}
fn refuse_unresolved_kernel_claims(c: &mut Counted, kernels: usize, claims: &[String]) {
if kernels > 0 || claims.is_empty() {
return;
}
c.violations += 1;
c.findings.push(LintFinding::new(
"PV-ONT-012",
RuleSeverity::Error,
format!(
"0 #[kernel] focus nodes resolved, yet the repo claims kernel(s): {} — the kernel shapes graded \
nothing; vacuous, not a pass",
claims.join(", ")
),
"contracts/ont-kernel-receipts-v1.yaml".to_string(),
));
}
fn vacuous_kernel_finding(ids: &[&str], severity: RuleSeverity, note: &str) -> LintFinding {
LintFinding::new(
"PV-ONT-012",
severity,
format!(
"0 #[kernel] symbols extracted: kernel shape(s) {}{note} graded nothing — vacuous, not a pass",
ids.join(", ")
),
"contracts/ont-kernel-receipts-v1.yaml".to_string(),
)
}
fn findings_of(
report: &Report,
arming: &ArmedShapes,
graph: &Graph,
extraction: &extract::Extraction,
) -> Counted {
let mut c = Counted {
findings: Vec::new(),
violations: 0,
warnings: 0,
unarmed_violations: 0,
};
for r in &report.results {
let armed = arming.is_armed(&r.shape);
let severity = match (armed, r.severity) {
(true, Severity::Violation) => {
c.violations += 1;
RuleSeverity::Error
}
(true, Severity::Warning) => {
c.warnings += 1;
RuleSeverity::Warning
}
(false, Severity::Violation) => {
c.unarmed_violations += 1;
RuleSeverity::Warning
}
(false, Severity::Warning) => RuleSeverity::Warning,
};
let rung = graph
.objects(&r.focus, &gguf::model("id"))
.first()
.and_then(|t| t.as_literal())
.map(|(id, _)| format!(" (rung {id})"))
.unwrap_or_default();
let mut f = LintFinding::new(
"PV-ONT-011",
severity,
format!(
"{}{rung} violates shape `{}`{} ({}): {}",
shapes::short(&r.focus),
r.shape,
if armed { "" } else { " [not armed]" },
r.component,
r.message
),
format!("contracts/{}.yaml", r.shape),
);
f.contract_stem = Some(r.shape.clone());
c.findings.push(f);
}
let refusals = extraction
.gguf
.errors
.iter()
.chain(&extraction.apr_model.errors)
.chain(&extraction.example.errors)
.chain(&extraction.readme.errors)
.chain(&extraction.llm_context.errors)
.chain(&extraction.csv.errors)
.chain(&extraction.ggplot2.errors)
.chain(&extraction.binary.errors);
for e in refusals {
c.violations += 1;
let mut f = LintFinding::new(
"PV-ONT-012",
RuleSeverity::Error,
format!("extractor refused {}: {}", e.file, e.what),
e.file.clone(),
);
f.contract_stem = None;
c.findings.push(f);
}
for e in &extraction.github.errors {
c.violations += 1;
let mut f = LintFinding::new(
"PV-ONT-012",
RuleSeverity::Error,
format!("extractor refused {}: {}", e.file, e.what),
e.file.clone(),
);
f.contract_stem = None;
c.findings.push(f);
}
c
}
fn subsumption_of(
contract_dir: &Path,
graph: &Graph,
shapes: &[NodeShape],
counted: &mut Counted,
) -> (usize, Vec<String>) {
let Some(sigma) = extract::sigma_of(contract_dir) else {
return (0, Vec::new());
};
for w in super::subsumption::weakenings(shapes, &sigma) {
counted.violations += 1;
let stem: String = w.split(' ').next().unwrap_or_default().to_string();
let mut f = LintFinding::new(
"PV-ONT-013",
RuleSeverity::Error,
format!("reject: {w} (R-19: a sub-concept may add constraints and may not remove any)"),
format!("contracts/{stem}.yaml"),
);
f.contract_stem = Some(stem);
counted.findings.push(f);
}
super::subsumption::inherited(graph, shapes, &sigma)
}
#[must_use]
pub fn validate_with_plant(
graph: &Graph,
shapes: &[NodeShape],
arming: &ArmedShapes,
) -> (Report, usize) {
let mut planted = graph.clone();
let plant = iri("contract", PLANT_ID);
let classes: BTreeSet<&str> = shapes
.iter()
.filter(|s| !s.target_class.is_empty())
.map(|s| s.target_class.as_str())
.collect();
for c in classes {
planted.insert(plant.clone(), RDF_TYPE, Term::iri(c));
}
let mut report = shapes::validate(&planted, shapes);
let plant_violations = report
.results
.iter()
.filter(|r| {
r.focus == plant && r.severity == Severity::Violation && arming.is_armed(&r.shape)
})
.count();
report.results.retain(|r| r.focus != plant);
report.focus_nodes_n = report.focus_nodes_n.saturating_sub(1);
(report, plant_violations)
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(name: &str) -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/fixtures/ont")
.join(name)
}
fn extra(result: &GateResult) -> (usize, usize, usize, Vec<String>, Vec<String>) {
match &result.extra {
Some(GateExtra::Shapes {
shapes_n,
focus_nodes_n,
plant_violations,
armed_shapes,
not_armed_shapes,
..
}) => (
*shapes_n,
*focus_nodes_n,
*plant_violations,
armed_shapes.clone(),
not_armed_shapes.clone(),
),
other => panic!("{other:?}"),
}
}
fn kernel_shape(id: &str) -> NodeShape {
NodeShape {
id: id.to_string(),
target_class: crate::ontology::rdf::ont("KernelSymbol"),
closed: false,
ignored_properties: Vec::new(),
properties: Vec::new(),
allow_empty: None,
}
}
fn empty_counted() -> Counted {
Counted {
findings: Vec::new(),
violations: 0,
warnings: 0,
unarmed_violations: 0,
}
}
#[test]
fn measured_carries_the_sorted_verified_commands() {
let mut stats = extract::claims::DocStats::default();
stats.verified_commands.insert("zeta".to_string());
stats.verified_commands.insert("alpha".to_string());
assert_eq!(
measured(&stats).verified_commands,
vec!["alpha".to_string(), "zeta".to_string()]
);
}
#[test]
fn count_freshness_counts_one_violation_per_difference() {
let dir = std::env::temp_dir().join(format!("k9-freshness-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("tmp dir");
std::fs::write(
dir.join("lint-baseline.json"),
r#"{"readme":{"verified_commands":[]},"claude_md":{"verified_commands":[]}}"#,
)
.expect("write baseline");
let mut extraction = extract::Extraction::default();
extraction
.readme
.verified_commands
.insert("one".to_string());
extraction
.readme
.verified_commands
.insert("two".to_string());
let mut c = empty_counted();
count_freshness(&mut c, &dir, &extraction);
std::fs::remove_dir_all(&dir).ok();
assert_eq!(c.violations, 2, "{:?}", c.findings);
assert_eq!(c.findings.len(), 2);
assert!(c
.findings
.iter()
.all(|f| f.severity == RuleSeverity::Error && f.message.contains("F-33")));
}
#[test]
fn subsumption_of_reports_inherited_shapes() {
let dir = fixture("subsumption-inherit");
let (shapes, _, _) = prepare(&dir, &ShapesOptions::default()).expect("shapes");
let extraction = extract::all_with(&dir, None).expect("extract");
let sigma = extract::sigma_of(&dir).expect("Σ");
let want = crate::lint::subsumption::inherited(&extraction.graph, &shapes, &sigma);
assert!(
want.0 > 0 && !want.1.is_empty(),
"fixture inherits: {want:?}"
);
let mut c = empty_counted();
let got = subsumption_of(&dir, &extraction.graph, &shapes, &mut c);
assert_eq!(got, want);
assert!(got.1.iter().all(|r| r.contains(" <- ")), "{got:?}");
assert_eq!(c.violations, 0, "{:?}", c.findings);
}
#[test]
fn subsumption_of_counts_one_violation_per_weakening() {
let dir = fixture("subsumption-weaken");
let (shapes, _, _) = prepare(&dir, &ShapesOptions::default()).expect("shapes");
let extraction = extract::all_with(&dir, None).expect("extract");
let sigma = extract::sigma_of(&dir).expect("Σ");
let n = crate::lint::subsumption::weakenings(&shapes, &sigma).len();
assert!(n > 0, "fixture weakens");
let mut c = empty_counted();
let _ = subsumption_of(&dir, &extraction.graph, &shapes, &mut c);
assert_eq!(c.violations, n, "{:?}", c.findings);
assert_eq!(c.findings.len(), n);
assert!(c.findings.iter().all(|f| f.message.contains("R-19")));
}
#[test]
fn subsumption_of_without_sigma_is_empty() {
let dir = fixture("shapes-ok");
let (shapes, _, _) = prepare(&dir, &ShapesOptions::default()).expect("shapes");
let extraction = extract::all_with(&dir, None).expect("extract");
let mut c = empty_counted();
assert_eq!(
subsumption_of(&dir, &extraction.graph, &shapes, &mut c),
(0, Vec::new())
);
assert_eq!(c.violations, 0);
}
#[test]
fn falsify_ont4c4_unarmed_kernel_vacuity_never_feeds_the_meet() {
let shapes = [kernel_shape("kernel-parity"), kernel_shape("kernel-timing")];
let none = ArmedShapes::Listed(Vec::new());
let mut c = empty_counted();
refuse_vacuous_kernels(&mut c, &shapes, &none, 0);
assert_eq!(c.violations, 0, "{:?}", c.findings);
assert_eq!(c.unarmed_violations, 1);
assert_eq!(c.findings.len(), 1);
assert_eq!(c.findings[0].severity, RuleSeverity::Warning);
assert!(c.findings[0].message.contains("[not armed]"));
assert_eq!(verdict_of(&c, false), Verdict::Pass);
let one = ArmedShapes::Listed(vec!["kernel-parity".to_string()]);
let mut c = empty_counted();
refuse_vacuous_kernels(&mut c, &shapes, &one, 0);
assert_eq!(c.violations, 1);
assert_eq!(c.unarmed_violations, 1);
let err = c
.findings
.iter()
.find(|f| f.severity == RuleSeverity::Error)
.expect("armed vacuity is an Error");
assert!(err.message.contains("kernel-parity"), "{}", err.message);
assert!(!err.message.contains("kernel-timing"), "{}", err.message);
assert_eq!(verdict_of(&c, false), Verdict::Fail);
let mut c = empty_counted();
refuse_vacuous_kernels(&mut c, &shapes, &ArmedShapes::All, 3);
assert_eq!(
(c.violations, c.unarmed_violations, c.findings.len()),
(0, 0, 0)
);
}
#[test]
fn falsify_ont4c4_a_claimed_kernel_with_no_focus_node_fails_by_name() {
let claims = vec![
"binding ox::kernels::g".to_string(),
"1 receipt file(s) under evidence/kernels".to_string(),
];
let mut c = empty_counted();
refuse_unresolved_kernel_claims(&mut c, 0, &claims);
assert_eq!(c.violations, 1);
assert_eq!(c.findings.len(), 1);
assert_eq!(c.findings[0].severity, RuleSeverity::Error);
assert!(
c.findings[0].message.contains("ox::kernels::g"),
"{}",
c.findings[0].message
);
assert!(
c.findings[0].message.contains("evidence/kernels"),
"{}",
c.findings[0].message
);
assert_eq!(verdict_of(&c, false), Verdict::Fail);
let mut c = empty_counted();
refuse_unresolved_kernel_claims(&mut c, 1, &claims);
assert_eq!((c.violations, c.findings.len()), (0, 0));
let mut c = empty_counted();
refuse_unresolved_kernel_claims(&mut c, 0, &[]);
assert_eq!((c.violations, c.findings.len()), (0, 0));
}
#[test]
fn kernel_claims_names_only_kernel_rows_and_receipts() {
let tmp = tempfile::tempdir().unwrap();
let d = tmp.path().join("contracts");
std::fs::create_dir_all(&d).unwrap();
std::fs::write(
d.join("binding.yaml"),
"version: 1.0.0\ntarget_crate: k\nbindings:\n\
- contract: c-v1.yaml\n equation: g\n module_path: k::cpu\n function: g\n status: implemented\n\
- contract: c-v1.yaml\n equation: g\n module_path: ox::kernels\n function: g\n status: implemented\n kernel: true\n",
)
.unwrap();
assert_eq!(
kernel_claims(&d, 0),
vec!["binding ox::kernels::g".to_string()]
);
assert_eq!(
kernel_claims(&d, 2),
vec![
"binding ox::kernels::g".to_string(),
"2 receipt file(s) under evidence/kernels".to_string()
]
);
assert!(kernel_claims(&tmp.path().join("none"), 0).is_empty());
}
#[test]
fn a_conforming_fixture_corpus_passes_and_the_plant_fires() {
match run_shapes_gate(&fixture("shapes-ok")) {
ShapesOutcome::Ran { result, findings } => {
assert!(result.passed, "{findings:?}");
assert_eq!(result.verdict, Verdict::Pass);
let (shapes_n, focus, plant, armed, not_armed) = extra(&result);
assert_eq!(shapes_n, 1);
assert_eq!(focus, 3, "the plant is not counted");
assert_eq!(plant, 1, "one minCount property → exactly one");
assert_eq!(
armed,
vec!["shape-holder".to_string()],
"no baseline key → all armed"
);
assert!(not_armed.is_empty());
}
other => panic!("expected Ran, got {other:?}"),
}
}
#[test]
fn a_corpus_violation_fails_naming_focus_node_and_shape() {
match run_shapes_gate(&fixture("shapes-violation")) {
ShapesOutcome::Ran { result, findings } => {
assert!(!result.passed);
assert_eq!(result.verdict, Verdict::Fail);
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(
findings[0].message.contains("ont:contract/bad-KIND"),
"{}",
findings[0].message
);
assert!(
findings[0].message.contains("shape `shape-holder`"),
"{}",
findings[0].message
);
assert_eq!(findings[0].rule_id, "PV-ONT-011");
}
other => panic!("expected Ran, got {other:?}"),
}
}
#[test]
fn warnings_only_is_unknown_warn_not_pass() {
match run_shapes_gate(&fixture("shapes-warn")) {
ShapesOutcome::Ran { result, .. } => {
assert_eq!(result.verdict, Verdict::Unknown(Reason::Warn));
assert!(result.passed, "a warning is not a failure");
}
other => panic!("expected Ran, got {other:?}"),
}
}
#[test]
fn no_shapes_no_focus_and_unsupported_are_not_corpus_verdicts() {
assert!(matches!(
run_shapes_gate(&fixture("sigma-ok")),
ShapesOutcome::NoShapes { .. }
));
assert!(matches!(
run_shapes_gate(&fixture("shapes-nofocus")),
ShapesOutcome::NoFocus { shapes_n: 1 }
));
match run_shapes_gate(&fixture("shapes-unsupported")) {
ShapesOutcome::Unsupported(ShapeError::Unsupported { component, .. }) => {
assert_eq!(component, "targetNode");
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn a_shape_set_that_cannot_fire_is_a_positive_control_failure() {
assert!(matches!(
run_shapes_gate(&fixture("shapes-noplant")),
ShapesOutcome::PositiveControlFailed { .. }
));
}
fn ladder(name: &str) -> ShapesOutcome {
run_shapes_gate(&fixture(name).join("contracts"))
}
#[test]
fn an_all_green_ladder_passes_with_measured_armed_and_green_reported_only() {
match ladder("ladder-green") {
ShapesOutcome::Ran { result, findings } => {
assert_eq!(result.verdict, Verdict::Pass, "{findings:?}");
let (_, _, plant, armed, not_armed) = extra(&result);
assert!(plant >= 1);
assert_eq!(
armed,
vec!["ladder-measured".to_string(), "ont-ladder-v1".to_string()]
.into_iter()
.filter(|a| armed.contains(a))
.collect::<Vec<_>>()
);
assert!(armed.contains(&"ladder-measured".to_string()));
assert_eq!(not_armed, vec!["ladder-green".to_string()]);
match &result.extra {
Some(GateExtra::Shapes {
by_entity_type,
controls,
witnesses,
unarmed_violations,
..
}) => {
assert_eq!(by_entity_type.get("gguf"), Some(&2));
let pc_extract = &controls.pc_extract;
assert_eq!(pc_extract.get("gguf").map(String::as_str), Some("fired"));
assert_eq!(
pc_extract.get("apr-model").map(String::as_str),
Some("fired")
);
assert_eq!(*witnesses, 4, "two rungs × two hosts");
assert_eq!(*unarmed_violations, 0, "all green: ladder-green holds too");
}
other => panic!("{other:?}"),
}
}
other => panic!("expected Ran, got {other:?}"),
}
}
#[test]
fn a_fallback_backend_is_reported_by_the_unarmed_green_shape_and_does_not_fail_the_gate() {
match ladder("ladder-fallback") {
ShapesOutcome::Ran { result, findings } => {
assert_eq!(
result.verdict,
Verdict::Pass,
"ladder-green is not armed here"
);
let named: Vec<&LintFinding> = findings
.iter()
.filter(|f| f.message.contains("[not armed]"))
.collect();
assert_eq!(named.len(), 1, "{findings:?}");
assert!(
named[0].message.contains("missingGreenHost"),
"{}",
named[0].message
);
assert!(
named[0].message.contains("gx10"),
"names the host: {}",
named[0].message
);
match &result.extra {
Some(GateExtra::Shapes {
unarmed_violations, ..
}) => assert_eq!(*unarmed_violations, 1),
other => panic!("{other:?}"),
}
}
other => panic!("expected Ran, got {other:?}"),
}
}
#[test]
fn arming_the_green_shape_on_the_fallback_fixture_fails_naming_rung_and_host() {
match ladder("ladder-fallback-armed") {
ShapesOutcome::Ran { result, findings } => {
assert_eq!(result.verdict, Verdict::Fail);
let f = findings
.iter()
.find(|f| f.message.contains("ladder-green"))
.expect("the green shape fired");
assert!(f.message.contains("gx10"), "{}", f.message);
assert!(!f.message.contains("[not armed]"));
}
other => panic!("expected Ran, got {other:?}"),
}
}
#[test]
fn a_wrong_hex_in_a_receipt_row_fails_ladder_measured_naming_both() {
match ladder("ladder-wronghex") {
ShapesOutcome::Ran { result, findings } => {
assert_eq!(result.verdict, Verdict::Fail);
let f = findings
.iter()
.find(|f| f.message.contains("receiptHexMismatch"))
.expect("the mismatch fired");
assert!(f.message.contains("ladder-measured"), "{}", f.message);
assert!(f.message.contains("lambda.json"), "{}", f.message);
}
other => panic!("expected Ran, got {other:?}"),
}
}
#[test]
fn a_row_without_sha256_is_not_a_witness_so_a_rung_with_only_such_rows_fails_measured() {
match ladder("ladder-nosha") {
ShapesOutcome::Ran { result, findings } => {
assert_eq!(result.verdict, Verdict::Fail);
assert!(
findings.iter().any(|f| f.message.contains("parityReceipt")),
"{findings:?}"
);
match &result.extra {
Some(GateExtra::Shapes {
witnesses,
unmeasured_rows,
..
}) => {
assert_eq!(*witnesses, 0);
assert!(*unmeasured_rows >= 1);
}
other => panic!("{other:?}"),
}
}
other => panic!("expected Ran, got {other:?}"),
}
}
#[test]
fn no_receipt_file_at_all_is_a_decline_naming_the_directory() {
match ladder("ladder-noreceipts") {
ShapesOutcome::NoReceipts { dir, .. } => assert_eq!(dir, receipts::EVIDENCE_DIR),
other => panic!("expected NoReceipts, got {other:?}"),
}
}
#[test]
fn a_foreign_receipt_schema_is_refused_by_name() {
match ladder("ladder-badschema") {
ShapesOutcome::ExtractFailed(ExtractFailure::Receipt(e)) => {
assert!(e.file.ends_with("lambda.json"), "{}", e.file);
assert!(e.what.contains("refused by name"), "{}", e.what);
}
other => panic!("expected ExtractFailed, got {other:?}"),
}
}
#[test]
fn a_lying_apr_header_is_an_extractor_rejection_naming_the_file() {
match ladder("ladder-lyingapr") {
ShapesOutcome::Ran { result, findings } => {
assert_eq!(result.verdict, Verdict::Fail);
let f = findings
.iter()
.find(|f| f.rule_id == "PV-ONT-012")
.expect("PV-ONT-012");
assert!(f.message.contains("lying.apr"), "{}", f.message);
assert!(f.message.contains("header says 3"), "{}", f.message);
}
other => panic!("expected Ran, got {other:?}"),
}
}
#[test]
fn the_plant_must_fire_from_an_armed_shape() {
assert!(matches!(
ladder("ladder-plantunarmed"),
ShapesOutcome::PositiveControlFailed { .. }
));
}
#[test]
fn every_implemented_entity_type_in_sigma_has_an_extract_control_and_it_fires() {
let sigma_path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contracts/ontology.yaml");
let text = std::fs::read_to_string(&sigma_path).expect("contracts/ontology.yaml");
let sigma = crate::ontology::sigma::Sigma::from_yaml(&text).expect("Σ parses");
let implemented: BTreeSet<String> = sigma
.entity_types
.iter()
.filter(|e| e.implemented)
.map(|e| e.name.clone())
.collect();
assert!(
!implemented.is_empty(),
"Σ implements nothing — the comparison would be vacuous"
);
let controls = extract_controls();
let controlled: BTreeSet<String> = controls.keys().cloned().collect();
assert_eq!(
controlled, implemented,
"pc_extract keys (left) must equal Σ's implemented entity types (right)"
);
for (k, v) in &controls {
assert_eq!(v, "fired", "pc_extract.{k}");
}
}
#[test]
fn every_counted_entity_type_has_a_counting_arm() {
let x = extract::Extraction::default();
for name in COUNTED_ENTITY_TYPES {
assert!(
entity_count(name, &x).is_some(),
"{name} is listed as counted and has no arm"
);
}
}
#[test]
fn every_implemented_entity_type_in_sigma_is_a_by_entity_type_key() {
let contracts = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contracts");
let implemented = sigma_implemented(&contracts);
assert!(
implemented.contains_key("json") && implemented.contains_key("release-evidence"),
"Σ no longer implements the two #3624 types — the test would be vacuous: {implemented:?}"
);
let mut x = extract::Extraction::default();
let sigma = extract::sigma_of(&contracts).expect("Σ parses");
for e in sigma.entity_types.iter().filter(|e| e.vocabulary.is_some()) {
x.github.by_type.insert(e.name.clone(), 0);
}
let map = by_entity_type(&x, &implemented).expect("every implemented type has an arm");
for name in implemented.keys() {
assert!(
map.contains_key(name),
"by_entity_type lacks Σ-implemented {name}"
);
}
assert_eq!(
map.get("release-evidence"),
Some(&0),
"no subject: 0 by rule"
);
}
#[test]
fn an_implemented_type_without_a_counting_arm_is_refused_by_name() {
const PLANT: &str = "planted-no-arm";
assert!(
entity_count(PLANT, &extract::Extraction::default()).is_none(),
"{PLANT} has a counting arm — the plant is vacuous"
);
let flipped: BTreeMap<String, String> = [("gguf", "gguf"), (PLANT, PLANT)]
.iter()
.map(|(n, x)| (n.to_string(), x.to_string()))
.collect();
match by_entity_type(&extract::Extraction::default(), &flipped) {
Err(ShapeError::Malformed { what, .. }) => assert!(
what.contains(&format!(
"entity type {PLANT} is registered in Σ as implemented"
)),
"{what}"
),
other => panic!("expected a named refusal, got {other:?}"),
}
}
#[test]
fn a_pv_contract_entity_type_is_counted_by_its_entity_class() {
let mut x = extract::Extraction::default();
let study = crate::ontology::rdf::Term::Iri(extract::pv_contract::entity_class("study"));
for id in ["a", "b"] {
x.graph
.insert(format!("urn:t:{id}"), RDF_TYPE, study.clone());
}
let implemented: BTreeMap<String, String> =
[("study", "pv_contract"), ("claim", "pv_contract")]
.iter()
.map(|(n, e)| (n.to_string(), e.to_string()))
.collect();
let map = by_entity_type(&x, &implemented).expect("pv_contract types are counted");
assert_eq!((map.get("study"), map.get("claim")), (Some(&2), Some(&0)));
}
fn shape_over(id: &str, class: &str, allow_empty: Option<&str>) -> NodeShape {
NodeShape {
target_class: crate::ontology::rdf::ont(class),
allow_empty: allow_empty.map(str::to_string),
..kernel_shape(id)
}
}
#[test]
fn collect_shapes_counts_every_document_it_read() {
let dir = fixture("shapes-ok");
let (shapes, checked) = collect_shapes(&dir).expect("shapes-ok parses");
assert_eq!(shapes.len(), 1);
assert_eq!(shapes[0].0.id, "shape-holder");
let documents = crate::ontology::extract::pv_contract::documents(&dir).len();
assert!(documents >= 2, "the fixture must have several documents");
assert_eq!(checked, documents);
}
#[test]
fn focus_of_counts_each_shapes_instances_in_declaration_order() {
let mut g = Graph::new();
let kernel = Term::iri(crate::ontology::rdf::ont("KernelSymbol"));
for id in ["k1", "k2"] {
g.insert(format!("urn:t:{id}"), RDF_TYPE, kernel.clone());
}
let shapes = [
shape_over("kernels", "KernelSymbol", None),
shape_over("nothing", "NoSuchClassXyz", None),
];
assert_eq!(
focus_of(&g, &shapes),
vec![("kernels".to_string(), 2), ("nothing".to_string(), 0)]
);
}
#[test]
fn vacuities_names_zero_focus_shapes_and_flags_only_armed_non_empty_ones() {
let focus = vec![
("a".to_string(), 0),
("b".to_string(), 3),
("c".to_string(), 0),
];
let shapes = [
shape_over("a", "KernelSymbol", None),
shape_over("b", "KernelSymbol", None),
shape_over("c", "KernelSymbol", Some("empty by design")),
];
let names = vec!["a".to_string(), "c".to_string()];
let (v, armed) = vacuities(&focus, &shapes, &ArmedShapes::Listed(vec!["a".to_string()]));
assert_eq!((v, armed), (names.clone(), true));
let (v, armed) = vacuities(&focus, &shapes, &ArmedShapes::Listed(vec!["c".to_string()]));
assert_eq!((v, armed), (names.clone(), false));
let (v, armed) = vacuities(&focus, &shapes, &ArmedShapes::Listed(Vec::new()));
assert_eq!((v, armed), (names, false));
let live = vec![("b".to_string(), 3)];
assert_eq!(
vacuities(&live, &shapes, &ArmedShapes::All),
(vec![], false)
);
}
#[test]
fn by_shape_is_sorted_shape_equals_count() {
let focus = vec![("zeta".to_string(), 2), ("alpha".to_string(), 0)];
assert_eq!(by_shape(&focus), vec!["alpha=0", "zeta=2"]);
}
#[test]
fn entity_count_gguf_is_rungs_plus_files_read() {
let rung = |id: &str| extract::gguf::Rung {
id: id.to_string(),
sha256: String::new(),
arch: String::new(),
gguf: String::new(),
backends: Vec::new(),
hosts: Vec::new(),
required: true,
contract: String::new(),
};
let mut x = extract::Extraction::default();
x.gguf.rungs = vec![rung("r1"), rung("r2")];
x.gguf.files_read = 3;
assert_eq!(entity_count("gguf", &x), Some(5));
}
#[test]
fn harness_broken_declines_only_when_a_cause_is_named() {
assert!(harness_broken(&extract::Extraction::default()).is_none());
let mut x = extract::Extraction {
release: Some(extract::release_evidence::ReleaseStats {
crux: Some(extract::release_crux::CruxStats::default()),
..Default::default()
}),
..Default::default()
};
assert!(
harness_broken(&x).is_none(),
"a measured harness with no cause is not broken"
);
let cause = "model-x: positive control ALL_WRONG".to_string();
x.release = Some(extract::release_evidence::ReleaseStats {
crux: Some(extract::release_crux::CruxStats {
harness_broken: vec![cause.clone()],
..Default::default()
}),
..Default::default()
});
match harness_broken(&x) {
Some(ShapesOutcome::HarnessBroken { causes }) => assert_eq!(causes, vec![cause]),
other => panic!("expected HarnessBroken, got {other:?}"),
}
}
}
#[cfg(test)]
#[path = "kernel_registry_v1_tests.rs"]
mod kernel_registry_v1_tests;
#[cfg(test)]
#[path = "shapes_gate_ggplot2_tests.rs"]
mod ggplot2_tests;