#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use mif_problem::{OutputFormat, ToProblem};
const DEFAULT_CATALOG: &str = ".claude/enabled-packs.json";
const DEFAULT_CONFIG: &str = "harness.config.json";
const DEFAULT_REPORTS_DIR: &str = "reports";
const ONTOLOGY_SOURCE_ENV: &str = "MIF_ONTOLOGY_SOURCE";
#[derive(Parser)]
#[command(
name = "mif-rh-cli",
version,
about = "CLI for the mif-rh research-harness ontology engine"
)]
struct Cli {
#[arg(long, global = true, value_parser = ["pretty", "json"])]
format: Option<String>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Resolve {
finding: PathBuf,
#[arg(long)]
topic: Option<String>,
#[arg(long)]
catalog: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
map: Option<PathBuf>,
#[arg(long)]
root: Option<PathBuf>,
},
Review {
#[arg(long)]
topic: Vec<String>,
#[arg(long)]
strict: bool,
#[arg(long)]
reports_dir: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
catalog: Option<PathBuf>,
#[arg(long)]
followup: Option<PathBuf>,
#[arg(long)]
root: Option<PathBuf>,
#[arg(long)]
relationship_script: Option<PathBuf>,
#[arg(long)]
build_index: bool,
#[arg(long)]
index: Option<PathBuf>,
#[arg(long)]
suggest: bool,
#[arg(long, requires = "suggest")]
calibration: Option<PathBuf>,
},
SuggestType {
#[arg(required_unless_present = "finding", conflicts_with = "finding")]
text: Option<String>,
#[arg(long)]
finding: Option<PathBuf>,
#[arg(long, required_unless_present = "finding")]
topic: Option<String>,
#[arg(long)]
catalog: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
root: Option<PathBuf>,
#[arg(long, default_value_t = 10)]
limit: usize,
#[arg(long)]
calibration: Option<PathBuf>,
#[arg(long, requires = "finding")]
record: bool,
#[arg(long)]
index: Option<PathBuf>,
},
Calibrate {
#[arg(long)]
reports_dir: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
catalog: Option<PathBuf>,
#[arg(long)]
root: Option<PathBuf>,
#[arg(long, default_value_t = 0.95)]
target_precision: f32,
#[arg(long, default_value_t = 0.5)]
tier2_target: f32,
#[arg(long)]
sample: Option<usize>,
#[arg(long, default_value_t = 0)]
seed: u64,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
confusions: Option<PathBuf>,
},
ExpansionCandidates {
#[arg(long)]
index: Option<PathBuf>,
#[arg(long)]
calibration: Option<PathBuf>,
#[arg(long)]
out: Option<PathBuf>,
},
Ontology {
#[command(subcommand)]
action: OntologyCommand,
},
Harness {
#[command(subcommand)]
action: HarnessCommand,
},
}
#[derive(Subcommand)]
enum OntologyCommand {
Fetch {
ids: Vec<String>,
#[arg(long, conflicts_with = "ids")]
all_enabled: bool,
#[arg(long)]
root: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
source: Option<String>,
},
Sync {
#[arg(long)]
root: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
catalog: Option<PathBuf>,
},
LockCheck {
#[arg(long)]
root: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
},
SyncRegistry {
#[arg(long)]
root: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
catalog: Option<PathBuf>,
#[arg(long)]
source: Option<String>,
},
Author {
new_id: String,
#[arg(
required_unless_present = "from_clusters",
conflicts_with = "from_clusters"
)]
topic: Option<String>,
#[arg(long)]
from_clusters: Option<PathBuf>,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
reports_dir: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum HarnessCommand {
GoalVersion {
goal: PathBuf,
},
BumpVersion {
spec: String,
#[arg(long = "pack")]
packs: Vec<String>,
#[arg(long)]
date: Option<String>,
#[arg(long)]
check: bool,
#[arg(long)]
root: Option<PathBuf>,
},
CheckVersionBump {
base: Option<String>,
#[arg(long)]
root: Option<PathBuf>,
},
ProjectReport {
report: PathBuf,
#[arg(long)]
schema: PathBuf,
#[arg(long = "ref")]
refs: Vec<PathBuf>,
#[arg(long = "json-out")]
json_out: Option<PathBuf>,
},
SiteTogglePrimary {
value: String,
#[arg(long)]
config: Option<PathBuf>,
},
SiteTogglePlugin {
name: String,
state: String,
#[arg(long)]
config: Option<PathBuf>,
},
PackToggle {
pack: String,
state: String,
#[arg(long)]
config: Option<PathBuf>,
},
WrapSource {
#[arg(long)]
url: String,
#[arg(long = "content-type")]
content_type: String,
#[arg(long)]
namespace: String,
#[arg(long)]
slug: String,
#[arg(long)]
out: PathBuf,
#[arg(long)]
title: Option<String>,
#[arg(long = "content-file")]
content_file: Option<PathBuf>,
#[arg(long)]
content: Option<String>,
#[arg(long = "source-type")]
source_type: Option<String>,
#[arg(long)]
schema: PathBuf,
#[arg(long = "ref")]
refs: Vec<PathBuf>,
},
BuildGraph {
findings_dir: PathBuf,
out: Option<PathBuf>,
},
BuildIndex {
findings_dir: PathBuf,
out: Option<PathBuf>,
},
ResolveMembership {
topic: String,
version: Option<String>,
#[arg(long)]
root: Option<PathBuf>,
},
GraphStats {
graph: PathBuf,
},
RenderArtifact {
artifact: PathBuf,
channel: String,
out: PathBuf,
#[arg(long)]
slug: String,
#[arg(long)]
slugpath: String,
#[arg(long)]
created: String,
#[arg(long)]
version: u64,
#[arg(long)]
verification: Option<PathBuf>,
},
SynthesizeArtifact {
findings_dir: PathBuf,
genre: Option<String>,
out: Option<PathBuf>,
},
ImportCorpus {
src_findings_dir: PathBuf,
dest_dir: PathBuf,
topic: String,
#[arg(long)]
schema: PathBuf,
#[arg(long = "ref")]
refs: Vec<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
},
BuildConcordance {
reports_dir: PathBuf,
out: Option<PathBuf>,
},
ReconcileSession {
topic_reports_dir: PathBuf,
#[arg(long)]
schema: PathBuf,
#[arg(long = "ref")]
refs: Vec<PathBuf>,
#[arg(long)]
sample: PathBuf,
},
TopicMetadata {
topic: String,
#[arg(long)]
config: PathBuf,
#[arg(long)]
findings: PathBuf,
#[arg(long)]
goal: PathBuf,
},
SynthesizeCorpus {
concordance: PathBuf,
map_out: PathBuf,
markdown_out: PathBuf,
#[arg(long)]
preserved_insights: Option<String>,
},
AssertGraphMif {
graph: PathBuf,
},
CheckCitationIntegrity {
#[arg(required = true)]
findings: Vec<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
},
CheckShippableTyping {
reports_dir: PathBuf,
},
Falsify {
finding: PathBuf,
fixture: Option<PathBuf>,
},
CheckRelationshipTargets {
#[arg(long)]
reports_dir: PathBuf,
},
ValidateConcordance {
concordance: PathBuf,
#[arg(long)]
config: PathBuf,
#[arg(long)]
catalog: PathBuf,
#[arg(long)]
root: PathBuf,
},
CheckOntologyRegistry {
#[arg(long)]
root: PathBuf,
},
}
type CliError = mif_rh::MifRhError;
#[derive(Debug)]
struct Outcome {
message: String,
exit_code: u8,
}
const MAIN_STACK_SIZE: usize = 16 * 1024 * 1024;
fn main() -> ExitCode {
let handle = match std::thread::Builder::new()
.stack_size(MAIN_STACK_SIZE)
.spawn(run_main)
{
Ok(handle) => handle,
Err(source) => {
eprintln!("mif-rh-cli: failed to spawn the main worker thread: {source}");
return ExitCode::FAILURE;
},
};
handle.join().unwrap_or_else(|_| {
eprintln!("mif-rh-cli: main worker thread panicked");
ExitCode::FAILURE
})
}
fn run_main() -> ExitCode {
let cli = Cli::parse();
let format = OutputFormat::select(cli.format.as_deref(), std::io::stderr().is_terminal());
match run(&cli.command) {
Ok(outcome) => {
println!("{}", outcome.message);
ExitCode::from(outcome.exit_code)
},
Err(error) => {
eprintln!("{}", error.render(format));
ExitCode::from(error.to_problem().exit_code.unwrap_or(1))
},
}
}
fn run(command: &Command) -> Result<Outcome, CliError> {
match command {
Command::Resolve {
finding,
topic,
catalog,
config,
map,
root,
} => resolve(
finding,
topic.as_deref(),
catalog.as_deref(),
config.as_deref(),
map.as_deref(),
root.as_deref(),
),
Command::Review {
topic,
strict,
reports_dir,
config,
catalog,
followup,
root,
relationship_script,
build_index,
index,
suggest,
calibration,
} => review(&ReviewArgs {
topics: topic,
strict: *strict,
reports_dir: reports_dir.as_deref(),
config: config.as_deref(),
catalog: catalog.as_deref(),
followup: followup.as_deref(),
root: root.as_deref(),
relationship_script: relationship_script.as_deref(),
build_index: *build_index,
index: index.as_deref(),
suggest: *suggest,
calibration: calibration.as_deref(),
}),
Command::SuggestType {
text,
finding,
topic,
catalog,
config,
root,
limit,
calibration,
record,
index,
} => suggest_type_cmd(&SuggestTypeArgs {
text: text.as_deref(),
finding: finding.as_deref(),
topic: topic.as_deref(),
catalog: catalog.as_deref(),
config: config.as_deref(),
root: root.as_deref(),
limit: *limit,
calibration: calibration.as_deref(),
record: *record,
index: index.as_deref(),
}),
Command::Calibrate {
reports_dir,
config,
catalog,
root,
target_precision,
tier2_target,
sample,
seed,
out,
confusions,
} => calibrate_cmd(&CalibrateArgs {
reports_dir: reports_dir.as_deref(),
config: config.as_deref(),
catalog: catalog.as_deref(),
root: root.as_deref(),
target_precision: *target_precision,
tier2_target: *tier2_target,
sample: *sample,
seed: *seed,
out: out.as_deref(),
confusions: confusions.as_deref(),
}),
Command::ExpansionCandidates {
index,
calibration,
out,
} => expansion_candidates_cmd(index.as_deref(), calibration.as_deref(), out.as_deref()),
Command::Ontology { action } => ontology_cmd(action),
Command::Harness { action } => harness_cmd(action),
}
}
fn ontology_source_override(explicit: Option<&str>) -> Option<String> {
explicit
.map(str::to_string)
.or_else(|| std::env::var(ONTOLOGY_SOURCE_ENV).ok())
}
fn ontology_cmd(action: &OntologyCommand) -> Result<Outcome, CliError> {
match action {
OntologyCommand::Fetch {
ids,
all_enabled,
root,
config,
source,
} => ontology_fetch_cmd(
ids,
*all_enabled,
root.as_deref(),
config.as_deref(),
source.as_deref(),
),
OntologyCommand::Sync {
root,
config,
catalog,
} => ontology_sync_cmd(root.as_deref(), config.as_deref(), catalog.as_deref()),
OntologyCommand::LockCheck { root, config } => {
ontology_lock_check_cmd(root.as_deref(), config.as_deref())
},
OntologyCommand::SyncRegistry {
root,
config,
catalog,
source,
} => ontology_sync_registry_cmd(
root.as_deref(),
config.as_deref(),
catalog.as_deref(),
source.as_deref(),
),
OntologyCommand::Author {
new_id,
topic,
from_clusters,
out,
reports_dir,
} => ontology_author_cmd(
new_id,
topic.as_deref(),
from_clusters.as_deref(),
out.as_deref(),
reports_dir.as_deref(),
),
}
}
fn ontology_fetch_cmd(
ids: &[String],
all_enabled: bool,
root: Option<&Path>,
config: Option<&Path>,
source: Option<&str>,
) -> Result<Outcome, CliError> {
let root = effective_path(root, ".");
let resolved_source =
mif_rh::vendor::resolve_source(&root, ontology_source_override(source).as_deref());
let fetch_ids = if all_enabled {
let config_path = effective_path(config, DEFAULT_CONFIG);
enabled_ontology_ids_from_file(&config_path)?
} else {
ids.to_vec()
};
if fetch_ids.is_empty() {
return Ok(Outcome {
message: "ontology fetch: nothing to fetch (no ids given and no enabled ontologies \
configured)"
.to_string(),
exit_code: 0,
});
}
let report = mif_rh::vendor::fetch(&root, &resolved_source, &fetch_ids)?;
let message = if report.vendored.is_empty() {
"ontology fetch: nothing to fetch (all requested layers are committed base layers or \
already vendored)"
.to_string()
} else {
let names: Vec<String> = report
.vendored
.iter()
.map(|v| format!("{}@{}", v.id, v.version))
.collect();
format!(
"ontology fetch: vendored {} ({}); lock updated at {}",
report.vendored.len(),
names.join(", "),
root.join("ontologies.lock.json").display()
)
};
Ok(Outcome {
message,
exit_code: 0,
})
}
fn ontology_sync_cmd(
root: Option<&Path>,
config: Option<&Path>,
catalog: Option<&Path>,
) -> Result<Outcome, CliError> {
let root = effective_path(root, ".");
let config_path = effective_path(config, DEFAULT_CONFIG);
let catalog_path = effective_path(catalog, DEFAULT_CATALOG);
let report = mif_rh::vendor::sync_catalog(&root, &config_path, &catalog_path)?;
Ok(Outcome {
message: format!(
"ontology sync: cataloged {} ontolog{} -> {}",
report.cataloged,
if report.cataloged == 1 { "y" } else { "ies" },
catalog_path.display()
),
exit_code: 0,
})
}
fn ontology_lock_check_cmd(
root: Option<&Path>,
config: Option<&Path>,
) -> Result<Outcome, CliError> {
let root = effective_path(root, ".");
let config_path = effective_path(config, DEFAULT_CONFIG);
let report = mif_rh::vendor::lock_check(&root, &config_path)?;
Ok(format_lock_check_outcome(&report))
}
fn ontology_sync_registry_cmd(
root: Option<&Path>,
config: Option<&Path>,
catalog: Option<&Path>,
source: Option<&str>,
) -> Result<Outcome, CliError> {
let root = effective_path(root, ".");
let config_path = effective_path(config, DEFAULT_CONFIG);
let catalog_path = effective_path(catalog, DEFAULT_CATALOG);
let resolved_source =
mif_rh::vendor::resolve_source(&root, ontology_source_override(source).as_deref());
let report =
mif_rh::vendor::sync_registry(&root, &config_path, &catalog_path, &resolved_source)?;
let message = if report.discovered.is_empty() {
"ontology sync-registry: no new ontologies in the registry".to_string()
} else {
format!(
"ontology sync-registry: discovered and enabled {} new ontology(ies): {}",
report.discovered.len(),
report.discovered.join(", ")
)
};
Ok(Outcome {
message,
exit_code: 0,
})
}
#[derive(serde::Deserialize)]
struct ExpansionCandidatesFile {
clusters: Vec<mif_rh::ExpansionCandidate>,
}
fn ontology_author_cmd(
new_id: &str,
topic: Option<&str>,
from_clusters: Option<&Path>,
out: Option<&Path>,
reports_dir: Option<&Path>,
) -> Result<Outcome, CliError> {
let report = if let Some(clusters_path) = from_clusters {
let contents =
std::fs::read_to_string(clusters_path).map_err(|source| mif_rh::MifRhError::Io {
path: clusters_path.display().to_string(),
source,
})?;
let file: ExpansionCandidatesFile =
serde_json::from_str(&contents).map_err(|source| mif_rh::MifRhError::Json {
path: clusters_path.display().to_string(),
source,
})?;
let source_name = clusters_path.file_name().map_or_else(
|| clusters_path.display().to_string(),
|name| name.to_string_lossy().to_string(),
);
mif_rh::draft_from_clusters(&file.clusters, new_id, &source_name)?
} else {
let topic = topic.unwrap_or_default();
let reports_dir = effective_path(reports_dir, DEFAULT_REPORTS_DIR);
let map_path = reports_dir.join(topic).join("ontology-map.json");
let contents =
std::fs::read_to_string(&map_path).map_err(|source| mif_rh::MifRhError::Io {
path: map_path.display().to_string(),
source,
})?;
let records: Vec<mif_rh::MapRecord> =
serde_json::from_str(&contents).map_err(|source| mif_rh::MifRhError::Json {
path: map_path.display().to_string(),
source,
})?;
mif_rh::draft_from_topic(&records, new_id, topic)?
};
let out_path = out.map_or_else(
|| std::env::temp_dir().join(format!("{new_id}.ontology.yaml")),
Path::to_path_buf,
);
std::fs::write(&out_path, &report.yaml).map_err(|source| mif_rh::MifRhError::Io {
path: out_path.display().to_string(),
source,
})?;
Ok(Outcome {
message: format!(
"ontology author: drafted '{new_id}' with {} candidate type(s) -> {}",
report.type_count,
out_path.display()
),
exit_code: 0,
})
}
#[allow(clippy::too_many_lines)]
fn harness_cmd(action: &HarnessCommand) -> Result<Outcome, CliError> {
match action {
HarnessCommand::GoalVersion { goal } => harness_goal_version_cmd(goal),
HarnessCommand::BumpVersion {
spec,
packs,
date,
check,
root,
} => harness_bump_version_cmd(spec, packs, date.as_deref(), *check, root.as_deref()),
HarnessCommand::CheckVersionBump { base, root } => {
harness_check_version_bump_cmd(base.as_deref(), root.as_deref())
},
HarnessCommand::ProjectReport {
report,
schema,
refs,
json_out,
} => harness_project_report_cmd(report, schema, refs, json_out.as_deref()),
HarnessCommand::SiteTogglePrimary { value, config } => {
harness_site_toggle_primary_cmd(value, config.as_deref())
},
HarnessCommand::SiteTogglePlugin {
name,
state,
config,
} => harness_site_toggle_plugin_cmd(name, state, config.as_deref()),
HarnessCommand::PackToggle {
pack,
state,
config,
} => harness_pack_toggle_cmd(pack, state, config.as_deref()),
HarnessCommand::WrapSource {
url,
content_type,
namespace,
slug,
out,
title,
content_file,
content,
source_type,
schema,
refs,
} => harness_wrap_source_cmd(&WrapSourceArgs {
url,
content_type,
namespace,
slug,
out,
title: title.as_deref(),
content_file: content_file.as_deref(),
content: content.as_deref(),
source_type: source_type.as_deref(),
schema,
refs,
}),
HarnessCommand::BuildGraph { findings_dir, out } => {
harness_build_graph_cmd(findings_dir, out.as_deref())
},
HarnessCommand::BuildIndex { findings_dir, out } => {
harness_build_index_cmd(findings_dir, out.as_deref())
},
HarnessCommand::ResolveMembership {
topic,
version,
root,
} => harness_resolve_membership_cmd(topic, version.as_deref(), root.as_deref()),
HarnessCommand::GraphStats { graph } => harness_graph_stats_cmd(graph),
HarnessCommand::RenderArtifact {
artifact,
channel,
out,
slug,
slugpath,
created,
version,
verification,
} => harness_render_artifact_cmd(&RenderArtifactArgs {
artifact,
channel,
out,
slug,
slugpath,
created,
version: *version,
verification: verification.as_deref(),
}),
HarnessCommand::SynthesizeArtifact {
findings_dir,
genre,
out,
} => harness_synthesize_artifact_cmd(findings_dir, genre.as_deref(), out.as_deref()),
HarnessCommand::ImportCorpus {
src_findings_dir,
dest_dir,
topic,
schema,
refs,
config,
} => harness_import_corpus_cmd(&ImportCorpusArgs {
src_findings_dir,
dest_dir,
topic,
schema,
refs,
config: config.as_deref(),
}),
HarnessCommand::BuildConcordance { reports_dir, out } => {
harness_build_concordance_cmd(reports_dir, out.as_deref())
},
HarnessCommand::ReconcileSession {
topic_reports_dir,
schema,
refs,
sample,
} => harness_reconcile_session_cmd(topic_reports_dir, schema, refs, sample),
HarnessCommand::TopicMetadata {
topic,
config,
findings,
goal,
} => harness_topic_metadata_cmd(topic, config, findings, goal),
HarnessCommand::SynthesizeCorpus {
concordance,
map_out,
markdown_out,
preserved_insights,
} => harness_synthesize_corpus_cmd(
concordance,
map_out,
markdown_out,
preserved_insights.as_deref(),
),
HarnessCommand::AssertGraphMif { graph } => harness_assert_graph_mif_cmd(graph),
HarnessCommand::CheckCitationIntegrity { findings, config } => Ok(
harness_check_citation_integrity_cmd(findings, config.as_deref()),
),
HarnessCommand::CheckShippableTyping { reports_dir } => {
harness_check_shippable_typing_cmd(reports_dir)
},
HarnessCommand::Falsify { finding, fixture } => {
harness_falsify_cmd(finding, fixture.as_deref())
},
HarnessCommand::CheckRelationshipTargets { reports_dir } => {
harness_check_relationship_targets_cmd(reports_dir)
},
HarnessCommand::ValidateConcordance {
concordance,
config,
catalog,
root,
} => harness_validate_concordance_cmd(concordance, config, catalog, root),
HarnessCommand::CheckOntologyRegistry { root } => {
Ok(harness_check_ontology_registry_cmd(root))
},
}
}
fn harness_assert_graph_mif_cmd(graph_path: &Path) -> Result<Outcome, CliError> {
let assertion = mif_rh::assert_graph_mif_file(graph_path)?;
let mut lines: Vec<String> = assertion
.checks
.iter()
.map(|c| {
if c.passed {
format!(" graph: ok — {}", c.message)
} else {
format!(" graph: FAIL — {}", c.message)
}
})
.collect();
lines.push(if assertion.passed {
"assert-graph: PASS".to_string()
} else {
"assert-graph: FAIL".to_string()
});
Ok(Outcome {
message: lines.join("\n"),
exit_code: u8::from(!assertion.passed),
})
}
fn harness_check_citation_integrity_cmd(findings: &[PathBuf], config: Option<&Path>) -> Outcome {
let report = mif_rh::check_citation_integrity(findings, config);
let mut lines = report.violations.clone();
if report.ok() {
lines.push(format!(
"citation-integrity: PASS ({} file(s))",
report.files_checked
));
} else {
lines.push(format!(
"citation-integrity: FAIL ({} violation(s))",
report.violations.len()
));
}
Outcome {
message: lines.join("\n"),
exit_code: u8::from(!report.ok()),
}
}
fn harness_check_shippable_typing_cmd(reports_dir: &Path) -> Result<Outcome, CliError> {
let report = mif_rh::check_shippable_typing(reports_dir)?;
let topic = reports_dir
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default();
let message = if report.ok() {
"check-shippable-typing: all shippable findings carry a valid ontology type — ok"
.to_string()
} else {
let mut lines = vec![format!(
"check-shippable-typing: {} shippable finding(s) lack a valid ontology type — \
synthesis BLOCKED (fail closed):",
report.blockers.len()
)];
lines.extend(report.blockers.iter().cloned());
lines.push(format!(
"check-shippable-typing: unblock with /ontology-review --topic {topic} --enrich \
then /resume --topic {topic}"
));
lines.join("\n")
};
Ok(Outcome {
message,
exit_code: u8::from(!report.ok()),
})
}
fn harness_falsify_cmd(
finding_path: &Path,
fixture_path: Option<&Path>,
) -> Result<Outcome, CliError> {
let result = mif_rh::falsify(finding_path, fixture_path)?;
eprintln!("{}", result.log_line);
let json_text = serde_json::to_string_pretty(&result.finding).map_err(|source| {
mif_rh::MifRhError::JsonSerialize {
path: finding_path.display().to_string(),
source,
}
})?;
Ok(Outcome {
message: json_text,
exit_code: 0,
})
}
fn harness_validate_concordance_cmd(
concordance_path: &Path,
config_path: &Path,
catalog_path: &Path,
root: &Path,
) -> Result<Outcome, CliError> {
let contents =
std::fs::read_to_string(concordance_path).map_err(|source| mif_rh::MifRhError::Io {
path: concordance_path.display().to_string(),
source,
})?;
let concordance: serde_json::Value =
serde_json::from_str(&contents).map_err(|source| mif_rh::MifRhError::Json {
path: concordance_path.display().to_string(),
source,
})?;
let catalog = mif_rh::Catalog::load(catalog_path)?;
let config = mif_rh::HarnessConfig::load(config_path)?;
let result = mif_rh::validate_concordance(&concordance, &config, &catalog, root)?;
let message = if result.ok() {
format!(
"validate-concordance: conformant ({} nodes, {} edges)",
result.nodes, result.edges
)
} else {
let mut lines = result.violations.clone();
lines.push(format!(
"validate-concordance: {} conformance violation(s) — fail",
result.violations.len()
));
if result
.violations
.iter()
.any(|v| v.contains("/ontology-review"))
{
lines.push(
"validate-concordance: bind/enrich the named topic(s) with /ontology-review \
--topic <id> --enrich, then rebuild."
.to_string(),
);
}
lines.join("\n")
};
Ok(Outcome {
message,
exit_code: u8::from(!result.ok()),
})
}
fn harness_check_ontology_registry_cmd(root: &Path) -> Outcome {
let result = mif_rh::validate_ontology_registry(root);
let render = |orphans: &[String]| {
if orphans.is_empty() {
"none".to_string()
} else {
orphans.join(" ")
}
};
let message = format!(
"ontology-registry: {} type(s) across the registry\n\
ontology-registry: relationship-endpoint orphans: {}\n\
ontology-registry: subtype_of-parent orphans: {}",
result.registry_type_count,
render(&result.relationship_endpoint_orphans),
render(&result.subtype_of_orphans),
);
Outcome {
message,
exit_code: u8::from(!result.ok()),
}
}
fn harness_check_relationship_targets_cmd(reports_dir: &Path) -> Result<Outcome, CliError> {
let report = mif_rh::check_relationship_targets(reports_dir)?;
let message = if report.ok() {
format!(
"check-relationship-targets: ok ({} relationship target(s) checked across {} \
active finding(s), 0 orphans)",
report.checked, report.active_findings
)
} else {
let mut lines: Vec<String> = report
.orphans
.iter()
.map(|o| format!("ORPHAN\t{}\t{}", o.source_file, o.target))
.collect();
lines.push(
"check-relationship-targets: one or more relationships[].target values do not \
resolve to any active finding @id (see ORPHAN lines above)"
.to_string(),
);
lines.join("\n")
};
Ok(Outcome {
message,
exit_code: u8::from(!report.ok()),
})
}
fn harness_goal_version_cmd(goal_path: &Path) -> Result<Outcome, CliError> {
let contents = std::fs::read_to_string(goal_path).map_err(|source| mif_rh::MifRhError::Io {
path: goal_path.display().to_string(),
source,
})?;
let goal: serde_json::Value =
serde_json::from_str(&contents).map_err(|source| mif_rh::MifRhError::Json {
path: goal_path.display().to_string(),
source,
})?;
Ok(Outcome {
message: mif_rh::goal_version_id(&goal),
exit_code: 0,
})
}
fn harness_bump_version_cmd(
spec: &str,
packs: &[String],
date: Option<&str>,
check: bool,
root: Option<&Path>,
) -> Result<Outcome, CliError> {
let root = effective_path(root, ".");
let report = mif_rh::bump_version(&mif_rh::BumpOptions {
root: &root,
spec,
packs,
date,
check,
})?;
let message = if report.applied {
format!(
"bump-version: {} -> {} (date {}){}",
report.old_version,
report.new_version,
report.date,
if report.packs.is_empty() {
String::new()
} else {
format!("; packs: {}", report.packs.join(", "))
}
)
} else {
format!(
"bump-version: --check, {} -> {} (date {}) — no files written",
report.old_version, report.new_version, report.date
)
};
Ok(Outcome {
message,
exit_code: 0,
})
}
fn harness_check_version_bump_cmd(
base: Option<&str>,
root: Option<&Path>,
) -> Result<Outcome, CliError> {
let root = effective_path(root, ".");
let base = base.unwrap_or("origin/main");
let report = mif_rh::check_version_bump(&root, base)?;
let mut lines: Vec<String> = report
.failures
.iter()
.map(|failure| match failure {
mif_rh::VersionGateFailure::PackNotBumped { pack, version } => format!(
"FAIL: {pack} changed but its plugin.json .version stayed at {version} — bump it"
),
mif_rh::VersionGateFailure::SkillNotBumped { skill, version } => format!(
"FAIL: {skill} changed but its SKILL.md version stayed at {version} — bump it"
),
mif_rh::VersionGateFailure::PointerNotAhead {
current,
last_release,
} => format!(
"FAIL: harness.config.json .version ({current}) is not ahead of the last \
release (v{last_release}) — someone needs to bump it before the next release"
),
mif_rh::VersionGateFailure::PointerMissing => {
"FAIL: harness.config.json has no .version".to_string()
},
})
.collect();
if lines.is_empty() {
lines.push(format!(
"check-version-bump: changed components moved their own version, and the release \
pointer ({}) is ahead of the last release",
report.pointer_at_head.as_deref().unwrap_or("-")
));
}
Ok(Outcome {
message: lines.join("\n"),
exit_code: u8::from(!report.failures.is_empty()),
})
}
fn harness_project_report_cmd(
report: &Path,
schema: &Path,
refs: &[PathBuf],
json_out: Option<&Path>,
) -> Result<Outcome, CliError> {
let projected = mif_rh::project_report(report, schema, refs)?;
if let Some(out_path) = json_out {
let text = serde_json::to_string_pretty(&projected).map_err(|source| {
mif_rh::MifRhError::JsonSerialize {
path: out_path.display().to_string(),
source,
}
})?;
std::fs::write(out_path, format!("{text}\n")).map_err(|source| mif_rh::MifRhError::Io {
path: out_path.display().to_string(),
source,
})?;
}
Ok(Outcome {
message: format!(
"mif-project: {} projects to a valid MIF L3 finding",
report.display()
),
exit_code: 0,
})
}
fn harness_site_toggle_primary_cmd(
value: &str,
config: Option<&Path>,
) -> Result<Outcome, CliError> {
let config_path = effective_path(config, DEFAULT_CONFIG);
mif_rh::site_toggle_primary(&config_path, value)?;
Ok(Outcome {
message: format!(
"site-toggle: primarySurface -> {value} in {}",
config_path.display()
),
exit_code: 0,
})
}
fn harness_site_toggle_plugin_cmd(
name: &str,
state: &str,
config: Option<&Path>,
) -> Result<Outcome, CliError> {
let enabled = parse_on_off("site-toggle", state)?;
let config_path = effective_path(config, DEFAULT_CONFIG);
mif_rh::site_toggle_plugin(&config_path, name, enabled)?;
Ok(Outcome {
message: format!(
"site-toggle: plugin {name} -> enabled={enabled} in {}",
config_path.display()
),
exit_code: 0,
})
}
fn harness_pack_toggle_cmd(
pack: &str,
state: &str,
config: Option<&Path>,
) -> Result<Outcome, CliError> {
let enabled = parse_on_off("pack-toggle", state)?;
let config_path = effective_path(config, DEFAULT_CONFIG);
mif_rh::pack_toggle(&config_path, pack, enabled)?;
Ok(Outcome {
message: format!(
"pack-toggle: {pack} -> enabled={enabled} in {}",
config_path.display()
),
exit_code: 0,
})
}
fn parse_on_off(caller: &str, state: &str) -> Result<bool, CliError> {
match state {
"on" => Ok(true),
"off" => Ok(false),
other => Err(mif_rh::MifRhError::InvalidToggleValue {
field: format!("{caller} state"),
value: other.to_string(),
allowed: "on|off".to_string(),
}),
}
}
struct WrapSourceArgs<'a> {
url: &'a str,
content_type: &'a str,
namespace: &'a str,
slug: &'a str,
out: &'a Path,
title: Option<&'a str>,
content_file: Option<&'a Path>,
content: Option<&'a str>,
source_type: Option<&'a str>,
schema: &'a Path,
refs: &'a [PathBuf],
}
fn harness_wrap_source_cmd(args: &WrapSourceArgs<'_>) -> Result<Outcome, CliError> {
let content_text =
mif_rh::read_source_content(args.content_file, args.content.unwrap_or_default())?;
let created = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let envelope = mif_rh::wrap_source(
&mif_rh::WrapSourceInputs {
url: args.url,
content_type: args.content_type,
namespace: args.namespace,
slug: args.slug,
title: args.title.unwrap_or_default(),
content: &content_text,
source_type: args.source_type.unwrap_or_default(),
created: &created,
},
args.schema,
args.refs,
)?;
if let Some(parent) = args.out.parent() {
std::fs::create_dir_all(parent).map_err(|source| mif_rh::MifRhError::Io {
path: parent.display().to_string(),
source,
})?;
}
let text = serde_json::to_string_pretty(&envelope).map_err(|source| {
mif_rh::MifRhError::JsonSerialize {
path: args.out.display().to_string(),
source,
}
})?;
std::fs::write(args.out, format!("{text}\n")).map_err(|source| mif_rh::MifRhError::Io {
path: args.out.display().to_string(),
source,
})?;
Ok(Outcome {
message: format!(
"wrap-source: wrote {} (urn:mif:source:{}:{}, {})",
args.out.display(),
args.namespace,
args.slug,
args.content_type
),
exit_code: 0,
})
}
fn harness_build_graph_cmd(findings_dir: &Path, out: Option<&Path>) -> Result<Outcome, CliError> {
let out_path = out.map_or_else(
|| findings_dir.join("../knowledge-graph.json"),
Path::to_path_buf,
);
let graph = mif_rh::build_graph(findings_dir)?;
let node_count = graph["nodes"].as_array().map_or(0, Vec::len);
let edge_count = graph["edges"].as_array().map_or(0, Vec::len);
let text = serde_json::to_string_pretty(&graph).map_err(|source| {
mif_rh::MifRhError::JsonSerialize {
path: out_path.display().to_string(),
source,
}
})?;
std::fs::write(&out_path, format!("{text}\n")).map_err(|source| mif_rh::MifRhError::Io {
path: out_path.display().to_string(),
source,
})?;
Ok(Outcome {
message: format!(
"build-graph: wrote {} ({node_count} nodes, {edge_count} edges)",
out_path.display()
),
exit_code: 0,
})
}
fn harness_build_index_cmd(findings_dir: &Path, out: Option<&Path>) -> Result<Outcome, CliError> {
let out_path = out.map_or_else(
|| findings_dir.join("../research-index.json"),
Path::to_path_buf,
);
let index = mif_rh::build_index(findings_dir)?;
let count = index["count"].as_u64().unwrap_or(0);
let text = serde_json::to_string_pretty(&index).map_err(|source| {
mif_rh::MifRhError::JsonSerialize {
path: out_path.display().to_string(),
source,
}
})?;
std::fs::write(&out_path, format!("{text}\n")).map_err(|source| mif_rh::MifRhError::Io {
path: out_path.display().to_string(),
source,
})?;
Ok(Outcome {
message: format!(
"build-index: wrote {} ({count} findings)",
out_path.display()
),
exit_code: 0,
})
}
fn harness_resolve_membership_cmd(
topic: &str,
version: Option<&str>,
root: Option<&Path>,
) -> Result<Outcome, CliError> {
let root = effective_path(root, ".");
let topic_dir = root.join("reports").join(topic);
let config_path = root.join(DEFAULT_CONFIG);
let version = if let Some(v) = version {
v.to_string()
} else {
let goal_path = topic_dir.join("goal.json");
let contents =
std::fs::read_to_string(&goal_path).map_err(|source| mif_rh::MifRhError::Io {
path: goal_path.display().to_string(),
source,
})?;
let goal: serde_json::Value =
serde_json::from_str(&contents).map_err(|source| mif_rh::MifRhError::Json {
path: goal_path.display().to_string(),
source,
})?;
mif_rh::goal_version_id(&goal)
};
let generated = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let report = mif_rh::resolve_membership(
&topic_dir,
&config_path,
&version,
&generated,
chrono::Utc::now(),
)?;
let members = report.members_file["members"]
.as_array()
.map_or(0, Vec::len);
let stale = report.members_file["stale"].as_array().map_or(0, Vec::len);
let excluded = report.members_file["excluded"]
.as_array()
.map_or(0, Vec::len);
let gaps: Vec<&str> = report.members_file["gap_dimensions"]
.as_array()
.into_iter()
.flatten()
.filter_map(|v| v.as_str())
.collect();
let gaps_display = if gaps.is_empty() {
"none".to_string()
} else {
gaps.join(", ")
};
Ok(Outcome {
message: format!(
"wrote {}\n members: {members} | stale: {stale} | excluded: {excluded} | gap_dimensions: {gaps_display}",
report.out_path.display()
),
exit_code: 0,
})
}
fn harness_graph_stats_cmd(graph: &Path) -> Result<Outcome, CliError> {
let contents = std::fs::read_to_string(graph).map_err(|source| mif_rh::MifRhError::Io {
path: graph.display().to_string(),
source,
})?;
let value: serde_json::Value =
serde_json::from_str(&contents).map_err(|source| mif_rh::MifRhError::Json {
path: graph.display().to_string(),
source,
})?;
let nodes = value["nodes"].as_array().map_or(0, Vec::len);
let edges = value["edges"].as_array().map_or(0, Vec::len);
Ok(Outcome {
message: format!("{nodes} {edges}"),
exit_code: 0,
})
}
struct RenderArtifactArgs<'a> {
artifact: &'a Path,
channel: &'a str,
out: &'a Path,
slug: &'a str,
slugpath: &'a str,
created: &'a str,
version: u64,
verification: Option<&'a Path>,
}
fn harness_render_artifact_cmd(args: &RenderArtifactArgs<'_>) -> Result<Outcome, CliError> {
let contents =
std::fs::read_to_string(args.artifact).map_err(|source| mif_rh::MifRhError::Io {
path: args.artifact.display().to_string(),
source,
})?;
let artifact: serde_json::Value =
serde_json::from_str(&contents).map_err(|source| mif_rh::MifRhError::Json {
path: args.artifact.display().to_string(),
source,
})?;
let verification = args
.verification
.map(|path| {
let text = std::fs::read_to_string(path).map_err(|source| mif_rh::MifRhError::Io {
path: path.display().to_string(),
source,
})?;
serde_json::from_str::<serde_json::Value>(&text).map_err(|source| {
mif_rh::MifRhError::Json {
path: path.display().to_string(),
source,
}
})
})
.transpose()?;
let rendered = mif_rh::render_artifact(
&mif_rh::RenderInputs {
artifact: &artifact,
slug: args.slug,
slugpath: args.slugpath,
created: args.created,
version: args.version,
verification: verification.as_ref(),
},
args.channel,
)?;
if let Some(parent) = args.out.parent() {
std::fs::create_dir_all(parent).map_err(|source| mif_rh::MifRhError::Io {
path: parent.display().to_string(),
source,
})?;
}
std::fs::write(args.out, &rendered).map_err(|source| mif_rh::MifRhError::Io {
path: args.out.display().to_string(),
source,
})?;
let line_count = rendered.lines().count();
Ok(Outcome {
message: format!(
"render: wrote {} ({}, {line_count} lines) from {}",
args.out.display(),
args.channel,
args.artifact.display()
),
exit_code: 0,
})
}
fn harness_synthesize_artifact_cmd(
findings_dir: &Path,
genre: Option<&str>,
out: Option<&Path>,
) -> Result<Outcome, CliError> {
let genre = genre.unwrap_or("general");
let out_path = out.map_or_else(|| findings_dir.join("../artifact.json"), Path::to_path_buf);
let artifact = mif_rh::synthesize_artifact(findings_dir, genre)?;
let section_count = artifact["sections"].as_array().map_or(0, Vec::len);
let source_count = artifact["sources"].as_array().map_or(0, Vec::len);
let text = serde_json::to_string_pretty(&artifact).map_err(|source| {
mif_rh::MifRhError::JsonSerialize {
path: out_path.display().to_string(),
source,
}
})?;
std::fs::write(&out_path, format!("{text}\n")).map_err(|source| mif_rh::MifRhError::Io {
path: out_path.display().to_string(),
source,
})?;
Ok(Outcome {
message: format!(
"synthesize: wrote {} (genre={genre}, {section_count} sections, {source_count} sources)",
out_path.display()
),
exit_code: 0,
})
}
struct ImportCorpusArgs<'a> {
src_findings_dir: &'a Path,
dest_dir: &'a Path,
topic: &'a str,
schema: &'a Path,
refs: &'a [PathBuf],
config: Option<&'a Path>,
}
fn harness_import_corpus_cmd(args: &ImportCorpusArgs<'_>) -> Result<Outcome, CliError> {
let report = mif_rh::import_corpus(
args.src_findings_dir,
args.dest_dir,
args.topic,
args.config,
args.schema,
args.refs,
)?;
let mut message = format!(
"import: imported {} finding(s) into {} (namespace {})",
report.imported,
args.dest_dir.display(),
report.namespace
);
if report.topic_registered {
use std::fmt::Write as _;
let _ = write!(message, "; registered topic '{}'", args.topic);
}
Ok(Outcome {
message,
exit_code: 0,
})
}
fn harness_build_concordance_cmd(
reports_dir: &Path,
out: Option<&Path>,
) -> Result<Outcome, CliError> {
let out_path = out.map_or_else(|| reports_dir.join("concordance.json"), Path::to_path_buf);
let concordance = mif_rh::build_concordance(reports_dir)?;
let node_count = concordance["nodes"].as_array().map_or(0, Vec::len);
let edge_count = concordance["edges"].as_array().map_or(0, Vec::len);
let text = serde_json::to_string_pretty(&concordance).map_err(|source| {
mif_rh::MifRhError::JsonSerialize {
path: out_path.display().to_string(),
source,
}
})?;
std::fs::write(&out_path, format!("{text}\n")).map_err(|source| mif_rh::MifRhError::Io {
path: out_path.display().to_string(),
source,
})?;
Ok(Outcome {
message: format!(
"build-concordance: wrote {} ({node_count} nodes, {edge_count} edges) across topics",
out_path.display()
),
exit_code: 0,
})
}
fn harness_reconcile_session_cmd(
topic_reports_dir: &Path,
schema: &Path,
refs: &[PathBuf],
sample: &Path,
) -> Result<Outcome, CliError> {
let report = mif_rh::reconcile_session(topic_reports_dir, schema, refs, sample)?;
let message = if report.plan.is_empty() {
"nothing to do".to_string()
} else {
format!("REMAINING WORK PLAN\n{}", report.plan.join("\n"))
};
Ok(Outcome {
message,
exit_code: 0,
})
}
fn harness_topic_metadata_cmd(
topic: &str,
config: &Path,
findings: &Path,
goal: &Path,
) -> Result<Outcome, CliError> {
let metadata = mif_rh::topic_metadata(topic, config, findings, goal)?;
Ok(Outcome {
message: metadata.to_shell_script(),
exit_code: 0,
})
}
fn harness_synthesize_corpus_cmd(
concordance: &Path,
map_out: &Path,
markdown_out: &Path,
preserved_insights: Option<&str>,
) -> Result<Outcome, CliError> {
let synthesis = mif_rh::synthesize_corpus(concordance, preserved_insights)?;
let sorted_map = mif_rh::sort_object_keys(&synthesis.map);
let map_text = serde_json::to_string_pretty(&sorted_map).map_err(|source| {
mif_rh::MifRhError::JsonSerialize {
path: map_out.display().to_string(),
source,
}
})?;
std::fs::write(map_out, format!("{map_text}\n")).map_err(|source| mif_rh::MifRhError::Io {
path: map_out.display().to_string(),
source,
})?;
std::fs::write(markdown_out, &synthesis.markdown).map_err(|source| mif_rh::MifRhError::Io {
path: markdown_out.display().to_string(),
source,
})?;
Ok(Outcome {
message: format!(
"synthesize-corpus: wrote {} and {} ({} topics, {} entities)",
map_out.display(),
markdown_out.display(),
synthesis.topics,
synthesis.entities
),
exit_code: 0,
})
}
fn enabled_ontology_ids_from_file(config_path: &Path) -> Result<Vec<String>, CliError> {
if !config_path.exists() {
return Err(mif_rh::MifRhError::ConfigMissing {
path: config_path.display().to_string(),
});
}
let contents =
std::fs::read_to_string(config_path).map_err(|source| mif_rh::MifRhError::Io {
path: config_path.display().to_string(),
source,
})?;
let config: serde_json::Value =
serde_json::from_str(&contents).map_err(|source| mif_rh::MifRhError::Json {
path: config_path.display().to_string(),
source,
})?;
let ids = config
.get("ontologies")
.and_then(serde_json::Value::as_array)
.map(|entries| {
entries
.iter()
.filter(|entry| {
entry
.get("enabled")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
})
.filter_map(|entry| entry.get("id").and_then(serde_json::Value::as_str))
.map(str::to_string)
.collect()
})
.unwrap_or_default();
Ok(ids)
}
fn format_lock_check_outcome(report: &mif_rh::LockCheckReport) -> Outcome {
let mut lines = Vec::new();
for id in &report.missing_pins {
lines.push(format!(
" MISSING PIN: '{id}' is enabled but absent from the lock — run `ontology fetch {id}`"
));
}
for id in &report.not_vendored {
lines.push(format!(
" NOT VENDORED: enabled '{id}' is pinned but its file is absent — run `ontology \
fetch {id}`"
));
}
for drift in &report.drift {
lines.push(format!(
" DRIFT: '{}' sha256 {} != pinned {} — a vendored ontology was edited locally; \
change it upstream and re-fetch",
drift.id, drift.got, drift.pinned
));
}
if report.ok() {
lines.push(format!(
"check-ontology-lock: ok ({} vendored ontolog{} match the lock)",
report.checked,
if report.checked == 1 { "y" } else { "ies" }
));
}
Outcome {
message: lines.join("\n"),
exit_code: u8::from(!report.ok()),
}
}
fn effective_path(given: Option<&Path>, default: &str) -> PathBuf {
given.map_or_else(|| PathBuf::from(default), Path::to_path_buf)
}
fn topic_from_path(finding: &Path) -> Option<String> {
let components: Vec<&str> = finding
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect();
let index = components.iter().position(|c| *c == "reports")?;
components.get(index + 1).map(|s| (*s).to_string())
}
fn resolve(
finding_path: &Path,
topic: Option<&str>,
catalog: Option<&Path>,
config: Option<&Path>,
map: Option<&Path>,
root: Option<&Path>,
) -> Result<Outcome, CliError> {
let catalog_path = effective_path(catalog, DEFAULT_CATALOG);
let config_path = effective_path(config, DEFAULT_CONFIG);
let root = effective_path(root, ".");
let topic = topic
.map(str::to_string)
.or_else(|| topic_from_path(finding_path))
.unwrap_or_default();
let finding = mif_rh::Finding::load(finding_path)?;
let catalog = mif_rh::Catalog::load(&catalog_path)?;
let config = mif_rh::HarnessConfig::load(&config_path)?;
let ontology_packs = mif_rh::ontology_pack::load_packs_via_catalog(&catalog, &root)?;
let ctx = mif_rh::ResolveContext {
topic: &topic,
catalog: &catalog,
config: &config,
ontology_packs: &ontology_packs,
};
let record = mif_rh::resolve_finding(&finding, &ctx)?;
let map_path = map.map(Path::to_path_buf).or_else(|| {
let topic_dir = PathBuf::from("reports").join(&topic);
topic_dir
.is_dir()
.then(|| topic_dir.join("ontology-map.json"))
});
if let Some(map_path) = &map_path {
upsert_map_record(map_path, &record)?;
}
let message = format!(
"{}: {} -> {} (valid={})",
finding.id,
record.basis.label(),
record.resolved_ontology.as_deref().unwrap_or("-"),
record.valid
);
Ok(Outcome {
message,
exit_code: u8::from(!record.valid),
})
}
fn upsert_map_record(
map_path: &Path,
record: &mif_rh::MapRecord,
) -> Result<(), mif_rh::MifRhError> {
let mut records: Vec<mif_rh::MapRecord> = std::fs::read_to_string(map_path)
.ok()
.and_then(|contents| serde_json::from_str(&contents).ok())
.unwrap_or_default();
records.retain(|r| r.finding_id != record.finding_id);
records.push(record.clone());
records.sort_by(|a, b| a.finding_id.cmp(&b.finding_id));
mif_rh::write_json_atomic(map_path, &records)
}
struct SuggestTypeArgs<'a> {
text: Option<&'a str>,
finding: Option<&'a Path>,
topic: Option<&'a str>,
catalog: Option<&'a Path>,
config: Option<&'a Path>,
root: Option<&'a Path>,
limit: usize,
calibration: Option<&'a Path>,
record: bool,
index: Option<&'a Path>,
}
const DEFAULT_CALIBRATION: &str = "reports/_meta/confidence-calibration.json";
const DEFAULT_INDEX: &str = "reports/_meta/search-index.sqlite";
fn run_id() -> String {
let epoch_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
format!("{epoch_secs}-{}", std::process::id())
}
fn is_expansion_miss(suggestions: &[mif_rh::TypeSuggestion]) -> bool {
suggestions
.first()
.is_none_or(|top| top.tier == mif_ontology::ConfidenceTier::TriggerExpansion)
}
fn suggest_type_cmd(args: &SuggestTypeArgs<'_>) -> Result<Outcome, CliError> {
let catalog_path = effective_path(args.catalog, DEFAULT_CATALOG);
let config_path = effective_path(args.config, DEFAULT_CONFIG);
let root = effective_path(args.root, ".");
let calibration_path = effective_path(args.calibration, DEFAULT_CALIBRATION);
let (query, topic, finding_id) = if let Some(finding_path) = args.finding {
let finding = mif_rh::Finding::load(finding_path)?;
let topic = args
.topic
.map(str::to_string)
.or_else(|| topic_from_path(finding_path))
.unwrap_or_default();
(mif_rh::index_text(&finding), topic, Some(finding.id))
} else {
(
args.text.unwrap_or_default().to_string(),
args.topic.unwrap_or_default().to_string(),
None,
)
};
let catalog = mif_rh::Catalog::load(&catalog_path)?;
let config = mif_rh::HarnessConfig::load(&config_path)?;
let ontology_packs = mif_rh::ontology_pack::load_packs_via_catalog(&catalog, &root)?;
let ctx = mif_rh::ResolveContext {
topic: &topic,
catalog: &catalog,
config: &config,
ontology_packs: &ontology_packs,
};
let cal = mif_ontology::CalibrationConfig::load_or_default(&calibration_path)
.map_err(mif_rh::MifRhError::from)?;
let embedder = mif_embed::Embedder::load()?;
let candidates = mif_rh::suggest::build_candidates(&ctx, &embedder, &cal)?;
let query_vector = embedder.embed(&query)?;
let suggestions =
mif_rh::suggest::suggest_from_candidates(&query_vector, &candidates, &cal, args.limit);
if args.record && is_expansion_miss(&suggestions) {
if let Some(finding_id) = finding_id {
let index_path = effective_path(args.index, DEFAULT_INDEX);
if let Some(parent) = index_path.parent() {
std::fs::create_dir_all(parent).map_err(|source| mif_rh::MifRhError::Io {
path: parent.display().to_string(),
source,
})?;
}
let index = mif_rh::FindingIndex::open(&index_path)?;
index.record_miss(&mif_rh::Miss {
finding_id,
topic: topic.clone(),
content: query,
vector: query_vector,
run_id: run_id(),
model: mif_embed::MODEL_ID.to_string(),
})?;
}
}
let message =
serde_json::to_string_pretty(&suggestions).map_err(|source| CliError::JsonSerialize {
path: "<stdout>".to_string(),
source,
})?;
Ok(Outcome {
message,
exit_code: 0,
})
}
struct CalibrateArgs<'a> {
reports_dir: Option<&'a Path>,
config: Option<&'a Path>,
catalog: Option<&'a Path>,
root: Option<&'a Path>,
target_precision: f32,
tier2_target: f32,
sample: Option<usize>,
seed: u64,
out: Option<&'a Path>,
confusions: Option<&'a Path>,
}
fn calibrate_cmd(args: &CalibrateArgs<'_>) -> Result<Outcome, CliError> {
let reports_dir = effective_path(args.reports_dir, DEFAULT_REPORTS_DIR);
let config_path = effective_path(args.config, DEFAULT_CONFIG);
let catalog_path = effective_path(args.catalog, DEFAULT_CATALOG);
let root = effective_path(args.root, ".");
let out = args.out.map_or_else(
|| reports_dir.join("_meta/confidence-calibration.json"),
Path::to_path_buf,
);
let catalog = mif_rh::Catalog::load(&catalog_path)?;
let config = mif_rh::HarnessConfig::load(&config_path)?;
let ontology_packs = mif_rh::ontology_pack::load_packs_via_catalog(&catalog, &root)?;
let embedder = mif_embed::Embedder::load()?;
let opts = mif_rh::CalibrateOptions {
target_precision: args.target_precision,
tier2_target: args.tier2_target,
sample: args.sample,
seed: args.seed,
};
let mut samples = Vec::new();
for topic in &config.topics {
let ctx = mif_rh::ResolveContext {
topic: &topic.id,
catalog: &catalog,
config: &config,
ontology_packs: &ontology_packs,
};
samples.extend(mif_rh::collect_topic_samples(
&reports_dir,
&ctx,
&embedder,
)?);
}
let samples = mif_rh::subsample(samples, &opts);
let confusions_note = if let Some(confusions_path) = args.confusions {
let report = mif_rh::confusions(&samples);
if let Some(parent) = confusions_path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent).map_err(|source| mif_rh::MifRhError::Io {
path: parent.display().to_string(),
source,
})?;
}
mif_rh::write_json_atomic(confusions_path, &report)?;
format!(
", {} confusion pair(s) written to {}",
report.pairs.len(),
confusions_path.display()
)
} else {
String::new()
};
let mut cal = mif_rh::sweep(&samples, &opts, &out)?;
let scored_topics: std::collections::BTreeSet<&str> =
samples.iter().map(|s| s.topic.as_str()).collect();
let mut negatives_active = false;
for topic in &config.topics {
if !scored_topics.contains(topic.id.as_str()) {
continue;
}
let ctx = mif_rh::ResolveContext {
topic: &topic.id,
catalog: &catalog,
config: &config,
ontology_packs: &ontology_packs,
};
if mif_rh::packs_carry_negatives(mif_rh::build_allowed(&ctx)?) {
negatives_active = true;
break;
}
}
cal.negatives_active = negatives_active;
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent).map_err(|source| mif_rh::MifRhError::Io {
path: parent.display().to_string(),
source,
})?;
}
mif_rh::write_json_atomic(&out, &cal)?;
let message = format!(
"calibrate: {} sample(s) -> tier1_floor={:.2} tier1_margin={:.2} tier2_floor={:.2} \
(method {}, written to {}{confusions_note})",
samples.len(),
cal.tier1_floor,
cal.tier1_margin,
cal.tier2_floor,
cal.method.as_deref().unwrap_or("-"),
out.display()
);
Ok(Outcome {
message,
exit_code: 0,
})
}
fn expansion_candidates_cmd(
index: Option<&Path>,
calibration: Option<&Path>,
out: Option<&Path>,
) -> Result<Outcome, CliError> {
let index_path = effective_path(index, DEFAULT_INDEX);
let calibration_path = effective_path(calibration, DEFAULT_CALIBRATION);
let cal = mif_ontology::CalibrationConfig::load_or_default(&calibration_path)
.map_err(mif_rh::MifRhError::from)?;
let misses = if index_path.exists() {
let idx = mif_rh::FindingIndex::open(&index_path)?;
idx.misses()?
} else {
Vec::new()
};
let misses: Vec<mif_rh::Miss> = misses
.into_iter()
.filter(|m| m.model == mif_embed::MODEL_ID)
.collect();
let candidates = mif_rh::expansion_candidates(&misses, &cal.expansion);
let payload = serde_json::json!({
"clusters": candidates,
"misses_considered": misses.len(),
"expansion": cal.expansion,
});
if let Some(out_path) = out {
mif_rh::write_json_atomic(out_path, &payload)?;
return Ok(Outcome {
message: format!(
"expansion-candidates: {} cluster(s) from {} miss(es) written to {}",
candidates.len(),
misses.len(),
out_path.display()
),
exit_code: 0,
});
}
let message =
serde_json::to_string_pretty(&payload).map_err(|source| CliError::JsonSerialize {
path: "<stdout>".to_string(),
source,
})?;
Ok(Outcome {
message,
exit_code: 0,
})
}
struct ReviewArgs<'a> {
topics: &'a [String],
strict: bool,
reports_dir: Option<&'a Path>,
config: Option<&'a Path>,
catalog: Option<&'a Path>,
followup: Option<&'a Path>,
root: Option<&'a Path>,
relationship_script: Option<&'a Path>,
build_index: bool,
index: Option<&'a Path>,
suggest: bool,
calibration: Option<&'a Path>,
}
struct SuggestPassInputs<'a> {
backlog: &'a mif_rh::FollowupBacklog,
catalog: &'a mif_rh::Catalog,
config: &'a mif_rh::HarnessConfig,
ontology_packs: &'a std::collections::HashMap<String, mif_rh::OntologyPack>,
meta_dir: &'a Path,
index: Option<&'a Path>,
calibration: Option<&'a Path>,
}
fn run_suggest_pass(inputs: &SuggestPassInputs<'_>) -> Result<String, CliError> {
let calibration_path = inputs.calibration.map_or_else(
|| inputs.meta_dir.join("confidence-calibration.json"),
Path::to_path_buf,
);
let cal = mif_ontology::CalibrationConfig::load_or_default(&calibration_path)
.map_err(mif_rh::MifRhError::from)?;
let embedder = mif_embed::Embedder::load()?;
let index_path = inputs.index.map_or_else(
|| inputs.meta_dir.join("search-index.sqlite"),
Path::to_path_buf,
);
let index = mif_rh::FindingIndex::open(&index_path)?;
let run = run_id();
let mut topic_ids: Vec<&String> = inputs.backlog.topics.keys().collect();
topic_ids.sort();
let (mut total_entries, mut topics_written, mut misses_recorded) = (0_usize, 0_usize, 0_usize);
for topic_id in topic_ids {
let ctx = mif_rh::ResolveContext {
topic: topic_id,
catalog: inputs.catalog,
config: inputs.config,
ontology_packs: inputs.ontology_packs,
};
let candidates = mif_rh::suggest::build_candidates(&ctx, &embedder, &cal)?;
let mut fresh = Vec::new();
for followup_entry in &inputs.backlog.topics[topic_id] {
let Some(file) = &followup_entry.file else {
continue;
};
let Ok(finding) = mif_rh::Finding::load(Path::new(file)) else {
continue;
};
let query = mif_rh::index_text(&finding);
if query.is_empty() {
continue;
}
let query_vector = embedder.embed(&query)?;
let suggestions = mif_rh::suggest::suggest_from_candidates(
&query_vector,
&candidates,
&cal,
mif_rh::suggest::SUGGESTION_DEPTH,
);
if is_expansion_miss(&suggestions) {
index.record_miss(&mif_rh::Miss {
finding_id: finding.id.clone(),
topic: topic_id.clone(),
vector: query_vector,
content: query,
run_id: run.clone(),
model: mif_embed::MODEL_ID.to_string(),
})?;
misses_recorded += 1;
}
fresh.push(mif_rh::SuggestionEntry {
finding_id: finding.id,
file: followup_entry.file.clone(),
basis: followup_entry.basis.clone(),
run_id: run.clone(),
candidates: suggestions,
status: mif_rh::queue::STATUS_PENDING.to_string(),
});
}
if fresh.is_empty() {
continue;
}
total_entries += fresh.len();
topics_written += 1;
let queue_path = inputs
.meta_dir
.join("suggestions")
.join(format!("{topic_id}.json"));
mif_rh::upsert_suggestions(&queue_path, topic_id, fresh)?;
}
if total_entries == 0 {
return Ok(format!(
"ontology-review: no findings needed suggestions; {misses_recorded} miss(es) recorded"
));
}
Ok(format!(
"ontology-review: suggestions written to {} ({} finding(s) across {} topic(s); {} \
miss(es) recorded)",
inputs.meta_dir.join("suggestions").display(),
total_entries,
topics_written,
misses_recorded,
))
}
fn format_topic_table(report: &mif_rh::ReviewReport) -> String {
use std::fmt::Write as _;
let mut out = format!(
"{:<28} {:<22} {:>6} {:>8} {:>10} {:>8} {:>9}\n",
"TOPIC", "BOUND", "FIND", "STAMPED", "DISCOVERY", "UNTYPED", "INVALID"
);
for topic in &report.topics {
let bound = if topic.bound.is_empty() {
"(core-only)".to_string()
} else {
topic.bound.join(",")
};
let bound: String = bound.chars().take(22).collect();
let _ = writeln!(
out,
"{:<28} {:<22} {:>6} {:>8} {:>10} {:>8} {:>9}",
topic.topic,
bound,
topic.total,
topic.stamped,
topic.discovery,
topic.untyped,
topic.bad
);
}
out.push_str("---");
out
}
fn relationship_script_path(given: Option<&Path>, root: &Path) -> Option<PathBuf> {
given.map(Path::to_path_buf).or_else(|| {
let candidate = root.join("scripts/check-relationship-targets.sh");
candidate.is_file().then_some(candidate)
})
}
fn review(args: &ReviewArgs<'_>) -> Result<Outcome, CliError> {
let reports_dir = effective_path(args.reports_dir, DEFAULT_REPORTS_DIR);
let config_path = effective_path(args.config, DEFAULT_CONFIG);
let catalog_path = effective_path(args.catalog, DEFAULT_CATALOG);
let root = effective_path(args.root, ".");
let meta_dir = reports_dir.join("_meta");
std::fs::create_dir_all(&meta_dir).map_err(|source| mif_rh::MifRhError::Io {
path: meta_dir.display().to_string(),
source,
})?;
let _lock = mif_rh::ReviewLock::acquire(&meta_dir.join(".review.lock"))?;
let catalog = mif_rh::Catalog::load(&catalog_path)?;
let config = mif_rh::HarnessConfig::load(&config_path)?;
let ontology_packs = mif_rh::ontology_pack::load_packs_via_catalog(&catalog, &root)?;
let relationship_script = relationship_script_path(args.relationship_script, &root);
let topic_ids: Option<Vec<String>> = (!args.topics.is_empty()).then(|| args.topics.to_vec());
let opts = mif_rh::ReviewOptions {
topics: topic_ids.as_deref(),
reports_dir: &reports_dir,
ontology_packs: &ontology_packs,
catalog: &catalog,
config: &config,
check_relationship_targets_script: relationship_script.as_deref(),
};
let (report, backlog) = mif_rh::review(&opts)?;
let mut message = format_topic_table(&report);
if let Some(followup_path) = args.followup {
use std::fmt::Write as _;
mif_rh::write_followup(followup_path, &backlog)?;
message.push('\n');
let _ = write!(
message,
"ontology-review: followup backlog written to {} ({} finding(s) across {} topic(s))",
followup_path.display(),
backlog.total_needs_followup,
backlog.topics.len(),
);
}
if args.suggest {
message.push('\n');
message.push_str(&run_suggest_pass(&SuggestPassInputs {
backlog: &backlog,
catalog: &catalog,
config: &config,
ontology_packs: &ontology_packs,
meta_dir: &meta_dir,
index: args.index,
calibration: args.calibration,
})?);
}
message.push('\n');
message.push_str(&report.summary_line());
if args.build_index {
let all_topic_ids: Vec<String> = config.topics.iter().map(|t| t.id.clone()).collect();
let index_path = args
.index
.map_or_else(|| meta_dir.join("search-index.sqlite"), Path::to_path_buf);
let mut index = mif_rh::FindingIndex::open(&index_path)?;
mif_rh::build_search_index(&reports_dir, &all_topic_ids, &mut index)?;
}
let exit_ok = if args.strict {
!report.strict_should_fail()
} else {
true
};
Ok(Outcome {
message,
exit_code: u8::from(!exit_ok),
})
}
#[cfg(test)]
mod tests {
use std::fs;
use super::{
CalibrateArgs, ImportCorpusArgs, RenderArtifactArgs, ReviewArgs, SuggestTypeArgs,
WrapSourceArgs, calibrate_cmd, expansion_candidates_cmd, harness_assert_graph_mif_cmd,
harness_build_concordance_cmd, harness_build_graph_cmd, harness_build_index_cmd,
harness_bump_version_cmd, harness_check_citation_integrity_cmd,
harness_check_ontology_registry_cmd, harness_check_relationship_targets_cmd,
harness_check_shippable_typing_cmd, harness_check_version_bump_cmd, harness_falsify_cmd,
harness_goal_version_cmd, harness_graph_stats_cmd, harness_import_corpus_cmd,
harness_pack_toggle_cmd, harness_project_report_cmd, harness_reconcile_session_cmd,
harness_render_artifact_cmd, harness_resolve_membership_cmd,
harness_site_toggle_plugin_cmd, harness_site_toggle_primary_cmd,
harness_synthesize_artifact_cmd, harness_synthesize_corpus_cmd, harness_topic_metadata_cmd,
harness_validate_concordance_cmd, harness_wrap_source_cmd, ontology_author_cmd,
ontology_fetch_cmd, ontology_lock_check_cmd, ontology_sync_cmd, ontology_sync_registry_cmd,
resolve, review, suggest_type_cmd, topic_from_path,
};
fn write_fixture(dir: &std::path::Path) {
fs::create_dir_all(dir.join(".claude")).unwrap();
fs::create_dir_all(dir.join("packs")).unwrap();
fs::create_dir_all(dir.join("reports/edu/findings")).unwrap();
fs::write(
dir.join("harness.config.json"),
r#"{"topics":[{"id":"edu","ontologies":["edu-fixture"]}]}"#,
)
.unwrap();
fs::write(
dir.join(".claude/enabled-packs.json"),
r#"{"ontologies":[{"id":"edu-fixture","version":"0.1.0","source":"packs/edu-fixture.yaml","core":false}]}"#,
)
.unwrap();
fs::write(
dir.join("packs/edu-fixture.yaml"),
"ontology:\n id: edu-fixture\n version: \"0.1.0\"\nentity_types:\n - name: title\n schema:\n required: [name]\n properties: {name: {type: string}}\n",
)
.unwrap();
fs::write(
dir.join("reports/edu/findings/good.json"),
r#"{"@id":"f-good","entity":{"name":"Algebra I","entity_type":"title"}}"#,
)
.unwrap();
fs::write(
dir.join("reports/edu/findings/invalid.json"),
r#"{"@id":"f-invalid","entity":{"entity_type":"title"}}"#,
)
.unwrap();
}
#[test]
fn resolve_a_valid_finding_exits_zero_and_writes_the_map() {
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
let map_path = dir.path().join("map.json");
let outcome = resolve(
&dir.path().join("reports/edu/findings/good.json"),
Some("edu"),
Some(&dir.path().join(".claude/enabled-packs.json")),
Some(&dir.path().join("harness.config.json")),
Some(&map_path),
Some(dir.path()),
)
.unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("resolved"));
assert!(map_path.exists());
}
#[test]
fn resolve_an_invalid_finding_exits_nonzero() {
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
let outcome = resolve(
&dir.path().join("reports/edu/findings/invalid.json"),
Some("edu"),
Some(&dir.path().join(".claude/enabled-packs.json")),
Some(&dir.path().join("harness.config.json")),
None,
Some(dir.path()),
)
.unwrap();
assert_eq!(outcome.exit_code, 1);
assert!(outcome.message.contains("valid=false"));
}
#[test]
fn suggest_type_prints_tier_annotated_json_and_exits_zero() {
if mif_embed::Embedder::load().is_err() {
eprintln!("skipping: embedding model unavailable in this environment");
return;
}
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
let outcome = suggest_type_cmd(&SuggestTypeArgs {
text: Some("A textbook titled Algebra I"),
finding: None,
topic: Some("edu"),
catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
config: Some(&dir.path().join("harness.config.json")),
root: Some(dir.path()),
limit: 10,
calibration: Some(&dir.path().join("absent-calibration.json")),
record: false,
index: None,
})
.unwrap();
assert_eq!(outcome.exit_code, 0);
let suggestions: serde_json::Value = serde_json::from_str(&outcome.message).unwrap();
let list = suggestions.as_array().unwrap();
assert!(list.is_empty());
}
#[test]
fn expansion_candidates_on_a_fresh_index_emit_an_empty_cluster_list() {
let dir = tempfile::tempdir().unwrap();
let index_path = dir.path().join("index.sqlite");
let outcome = expansion_candidates_cmd(
Some(&index_path),
Some(&dir.path().join("absent-calibration.json")),
None,
)
.unwrap();
assert_eq!(outcome.exit_code, 0);
let payload: serde_json::Value = serde_json::from_str(&outcome.message).unwrap();
assert!(payload["clusters"].as_array().unwrap().is_empty());
assert_eq!(payload["misses_considered"], 0);
}
fn enrich_fixture_pack(dir: &std::path::Path) {
fs::write(
dir.join("packs/edu-fixture.yaml"),
"ontology:\n id: edu-fixture\n version: \"0.1.0\"\nentity_types:\n - name: title\n description: A published educational title\n aliases: [textbook]\n schema:\n required: [name]\n properties: {name: {type: string}}\n",
)
.unwrap();
}
#[test]
fn calibrate_derives_a_wellformed_artifact_from_stamped_findings() {
if mif_embed::Embedder::load().is_err() {
eprintln!("skipping: embedding model unavailable in this environment");
return;
}
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
enrich_fixture_pack(dir.path());
resolve(
&dir.path().join("reports/edu/findings/good.json"),
Some("edu"),
Some(&dir.path().join(".claude/enabled-packs.json")),
Some(&dir.path().join("harness.config.json")),
Some(&dir.path().join("reports/edu/ontology-map.json")),
Some(dir.path()),
)
.unwrap();
let out = dir.path().join("reports/_meta/confidence-calibration.json");
let confusions_path = dir.path().join("reports/_meta/confusions.json");
let outcome = calibrate_cmd(&CalibrateArgs {
reports_dir: Some(&dir.path().join("reports")),
config: Some(&dir.path().join("harness.config.json")),
catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
root: Some(dir.path()),
target_precision: 1.0,
tier2_target: 0.5,
sample: None,
seed: 0,
out: Some(&out),
confusions: Some(&confusions_path),
})
.unwrap();
assert_eq!(outcome.exit_code, 0);
let cal = mif_ontology::CalibrationConfig::load_or_default(&out).unwrap();
assert!(cal.calibrated);
assert_eq!(cal.method.as_deref(), Some("stamped-quantile-v1"));
assert_eq!(cal.sample_size, Some(1));
assert!(cal.tier2_floor <= cal.tier1_floor);
assert!(!cal.negatives_active);
assert!(outcome.message.contains("confusion pair(s)"));
let report: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&confusions_path).unwrap()).unwrap();
assert_eq!(report["version"], "confusions-v1");
assert_eq!(report["sample_count"], 1);
assert!(report["pairs"].as_array().unwrap().is_empty());
}
#[test]
fn calibrate_records_negatives_participation_in_the_artifact() {
if mif_embed::Embedder::load().is_err() {
eprintln!("skipping: embedding model unavailable in this environment");
return;
}
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
fs::write(
dir.path().join("packs/edu-fixture.yaml"),
"ontology:\n id: edu-fixture\n version: \"0.1.0\"\nentity_types:\n - name: title\n description: A published educational title\n aliases: [textbook]\n negative_examples:\n - A lesson plan for teachers\n schema:\n required: [name]\n properties: {name: {type: string}}\n",
)
.unwrap();
resolve(
&dir.path().join("reports/edu/findings/good.json"),
Some("edu"),
Some(&dir.path().join(".claude/enabled-packs.json")),
Some(&dir.path().join("harness.config.json")),
Some(&dir.path().join("reports/edu/ontology-map.json")),
Some(dir.path()),
)
.unwrap();
let out = dir.path().join("reports/_meta/confidence-calibration.json");
let outcome = calibrate_cmd(&CalibrateArgs {
reports_dir: Some(&dir.path().join("reports")),
config: Some(&dir.path().join("harness.config.json")),
catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
root: Some(dir.path()),
target_precision: 1.0,
tier2_target: 0.5,
sample: None,
seed: 0,
out: Some(&out),
confusions: None,
})
.unwrap();
assert_eq!(outcome.exit_code, 0);
let cal = mif_ontology::CalibrationConfig::load_or_default(&out).unwrap();
assert!(cal.negatives_active);
}
#[test]
fn an_unbound_negatives_pack_never_claims_participation() {
if mif_embed::Embedder::load().is_err() {
eprintln!("skipping: embedding model unavailable in this environment");
return;
}
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
enrich_fixture_pack(dir.path());
fs::write(
dir.path().join("packs/unbound-fixture.yaml"),
"ontology:\n id: unbound-fixture\n version: \"0.1.0\"\nentity_types:\n - name: lesson\n description: A lesson plan\n negative_examples:\n - A published textbook\n",
)
.unwrap();
fs::write(
dir.path().join(".claude/enabled-packs.json"),
r#"{"ontologies":[{"id":"edu-fixture","version":"0.1.0","source":"packs/edu-fixture.yaml","core":false},{"id":"unbound-fixture","version":"0.1.0","source":"packs/unbound-fixture.yaml","core":false}]}"#,
)
.unwrap();
resolve(
&dir.path().join("reports/edu/findings/good.json"),
Some("edu"),
Some(&dir.path().join(".claude/enabled-packs.json")),
Some(&dir.path().join("harness.config.json")),
Some(&dir.path().join("reports/edu/ontology-map.json")),
Some(dir.path()),
)
.unwrap();
let out = dir.path().join("reports/_meta/confidence-calibration.json");
let outcome = calibrate_cmd(&CalibrateArgs {
reports_dir: Some(&dir.path().join("reports")),
config: Some(&dir.path().join("harness.config.json")),
catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
root: Some(dir.path()),
target_precision: 1.0,
tier2_target: 0.5,
sample: None,
seed: 0,
out: Some(&out),
confusions: None,
})
.unwrap();
assert_eq!(outcome.exit_code, 0);
let cal = mif_ontology::CalibrationConfig::load_or_default(&out).unwrap();
assert!(!cal.negatives_active);
}
#[test]
fn calibrate_writes_the_confusion_export_even_when_the_sweep_fails() {
if mif_embed::Embedder::load().is_err() {
eprintln!("skipping: embedding model unavailable in this environment");
return;
}
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
enrich_fixture_pack(dir.path());
let confusions_path = dir.path().join("reports/_meta/confusions.json");
let result = calibrate_cmd(&CalibrateArgs {
reports_dir: Some(&dir.path().join("reports")),
config: Some(&dir.path().join("harness.config.json")),
catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
root: Some(dir.path()),
target_precision: 1.0,
tier2_target: 0.5,
sample: None,
seed: 0,
out: Some(&dir.path().join("reports/_meta/confidence-calibration.json")),
confusions: Some(&confusions_path),
});
let error = result.map(|_| ()).unwrap_err();
assert!(error.to_string().contains("no stamped"));
let report: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&confusions_path).unwrap()).unwrap();
assert_eq!(report["version"], "confusions-v1");
assert_eq!(report["sample_count"], 0);
assert!(report["pairs"].as_array().unwrap().is_empty());
}
#[test]
fn review_suggest_writes_a_topic_queue_for_followup_findings() {
if mif_embed::Embedder::load().is_err() {
eprintln!("skipping: embedding model unavailable in this environment");
return;
}
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
enrich_fixture_pack(dir.path());
fs::write(
dir.path().join("reports/edu/findings/untyped.json"),
r#"{"@id":"f-untyped","content":"A fascinating textbook about geometry"}"#,
)
.unwrap();
let outcome = review(&ReviewArgs {
topics: &[],
strict: false,
reports_dir: Some(&dir.path().join("reports")),
config: Some(&dir.path().join("harness.config.json")),
catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
followup: None,
root: Some(dir.path()),
relationship_script: None,
build_index: false,
index: None,
suggest: true,
calibration: None,
})
.unwrap();
let lines: Vec<&str> = outcome.message.lines().collect();
let confirmation = lines
.iter()
.position(|l| l.starts_with("ontology-review: suggestions written"))
.expect("suggestions confirmation line present");
assert!(confirmation < lines.len() - 1);
let queue_path = dir.path().join("reports/_meta/suggestions/edu.json");
let queue: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&queue_path).unwrap()).unwrap();
let entries = queue["entries"].as_array().unwrap();
assert!(
entries
.iter()
.any(|e| e["finding_id"] == "f-untyped" && e["status"] == "pending")
);
}
#[test]
fn suggest_type_ranks_a_described_type_from_a_finding_query() {
if mif_embed::Embedder::load().is_err() {
eprintln!("skipping: embedding model unavailable in this environment");
return;
}
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
fs::write(
dir.path().join("packs/edu-fixture.yaml"),
"ontology:\n id: edu-fixture\n version: \"0.1.0\"\nentity_types:\n - name: title\n description: A published educational title\n aliases: [textbook]\n schema:\n required: [name]\n properties: {name: {type: string}}\n",
)
.unwrap();
let outcome = suggest_type_cmd(&SuggestTypeArgs {
text: None,
finding: Some(&dir.path().join("reports/edu/findings/good.json")),
topic: None, catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
config: Some(&dir.path().join("harness.config.json")),
root: Some(dir.path()),
limit: 10,
calibration: Some(&dir.path().join("absent-calibration.json")),
record: false,
index: None,
})
.unwrap();
assert_eq!(outcome.exit_code, 0);
let suggestions: serde_json::Value = serde_json::from_str(&outcome.message).unwrap();
let list = suggestions.as_array().unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0]["entity_type"], "title");
assert!(list[0]["tier"].is_string());
assert_eq!(list[0]["calibrated"], false);
}
#[test]
fn review_strict_fails_closed_on_an_invalid_finding_but_succeeds_without_strict() {
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
let strict_outcome = review(&ReviewArgs {
topics: &[],
strict: true,
reports_dir: Some(&dir.path().join("reports")),
config: Some(&dir.path().join("harness.config.json")),
catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
followup: None,
root: Some(dir.path()),
relationship_script: None,
build_index: false,
index: None,
suggest: false,
calibration: None,
})
.unwrap();
assert_eq!(strict_outcome.exit_code, 1);
assert!(strict_outcome.message.contains("1 invalid/unresolved"));
let lenient_outcome = review(&ReviewArgs {
topics: &[],
strict: false,
reports_dir: Some(&dir.path().join("reports")),
config: Some(&dir.path().join("harness.config.json")),
catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
followup: None,
root: Some(dir.path()),
relationship_script: None,
build_index: false,
index: None,
suggest: false,
calibration: None,
})
.unwrap();
assert_eq!(lenient_outcome.exit_code, 0);
}
#[test]
fn review_acquires_and_releases_the_lock_and_prints_the_topic_table() {
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
let outcome = review(&ReviewArgs {
topics: &[],
strict: false,
reports_dir: Some(&dir.path().join("reports")),
config: Some(&dir.path().join("harness.config.json")),
catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
followup: None,
root: Some(dir.path()),
relationship_script: None,
build_index: false,
index: None,
suggest: false,
calibration: None,
})
.unwrap();
assert!(outcome.message.contains("TOPIC"), "{}", outcome.message);
assert!(outcome.message.contains("edu"), "{}", outcome.message);
assert!(
!dir.path().join("reports/_meta/.review.lock").exists(),
"lock file must be released after review() returns"
);
}
#[test]
fn review_with_followup_prints_the_summary_line_last() {
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
let followup_path = dir.path().join("followup.json");
let outcome = review(&ReviewArgs {
topics: &[],
strict: false,
reports_dir: Some(&dir.path().join("reports")),
config: Some(&dir.path().join("harness.config.json")),
catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
followup: Some(&followup_path),
root: Some(dir.path()),
relationship_script: None,
build_index: false,
index: None,
suggest: false,
calibration: None,
})
.unwrap();
let last_line = outcome.message.lines().next_back().unwrap();
assert!(
last_line.starts_with("1 topic(s);"),
"last line should be the summary, got: {last_line}"
);
}
#[test]
fn review_with_build_index_populates_the_default_index_path() {
if mif_embed::Embedder::load().is_err() {
eprintln!("skipping: could not load embedding model");
return;
}
let dir = tempfile::tempdir().unwrap();
write_fixture(dir.path());
review(&ReviewArgs {
topics: &[],
strict: false,
reports_dir: Some(&dir.path().join("reports")),
config: Some(&dir.path().join("harness.config.json")),
catalog: Some(&dir.path().join(".claude/enabled-packs.json")),
followup: None,
root: Some(dir.path()),
relationship_script: None,
build_index: true,
index: None,
suggest: false,
calibration: None,
})
.unwrap();
assert!(
dir.path()
.join("reports/_meta/search-index.sqlite")
.exists()
);
}
#[test]
fn topic_from_path_derives_the_topic_from_a_reports_relative_finding_path() {
assert_eq!(
topic_from_path(std::path::Path::new("reports/edu/findings/f.json")),
Some("edu".to_string())
);
assert_eq!(
topic_from_path(std::path::Path::new("/some/other/path.json")),
None
);
}
#[test]
fn assert_graph_mif_cmd_passes_a_well_formed_graph() {
let dir = tempfile::tempdir().unwrap();
let graph_path = dir.path().join("knowledge-graph.json");
fs::write(
&graph_path,
r#"{"nodes":[{"id":"urn:mif:f1","kind":"concept"}],
"edges":[{"source":"urn:mif:f1","target":"urn:mif:f1","via":"relationship"}]}"#,
)
.unwrap();
let outcome = harness_assert_graph_mif_cmd(&graph_path).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("assert-graph: PASS"));
}
#[test]
fn assert_graph_mif_cmd_fails_on_a_tag_derived_id() {
let dir = tempfile::tempdir().unwrap();
let graph_path = dir.path().join("knowledge-graph.json");
fs::write(
&graph_path,
r#"{"nodes":[{"id":"some-tag","kind":"concept"}],
"edges":[{"source":"some-tag","target":"some-tag","via":"relationship"}]}"#,
)
.unwrap();
let outcome = harness_assert_graph_mif_cmd(&graph_path).unwrap();
assert_eq!(outcome.exit_code, 1);
assert!(outcome.message.contains("assert-graph: FAIL"));
assert!(
outcome
.message
.contains("FAIL — every node id is a urn:mif:")
);
}
#[test]
fn check_citation_integrity_cmd_passes_a_well_formed_finding() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("f.json");
fs::write(
&path,
r#"{"@id":"urn:mif:f1","citations":[{"url":"https://example.com","citationRole":"supports"}]}"#,
)
.unwrap();
let outcome = harness_check_citation_integrity_cmd(&[path], None);
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("citation-integrity: PASS"));
}
#[test]
fn check_citation_integrity_cmd_fails_on_no_citations() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("f.json");
fs::write(&path, r#"{"@id":"urn:mif:f1","citations":[]}"#).unwrap();
let outcome = harness_check_citation_integrity_cmd(&[path], None);
assert_eq!(outcome.exit_code, 1);
assert!(outcome.message.contains("citation-integrity: FAIL"));
}
#[test]
fn check_shippable_typing_cmd_passes_a_typed_shippable_finding() {
let dir = tempfile::tempdir().unwrap();
let reports_dir = dir.path().join("reports/edu");
fs::create_dir_all(reports_dir.join("findings")).unwrap();
fs::write(
reports_dir.join("findings/f1.json"),
r#"{"@id":"urn:mif:f1","extensions":{"harness":{"verification":{"verdict":"survived"}}}}"#,
)
.unwrap();
fs::write(
reports_dir.join("ontology-map.json"),
r#"[{"finding_id":"urn:mif:f1","valid":true,"basis":"declared"}]"#,
)
.unwrap();
let outcome = harness_check_shippable_typing_cmd(&reports_dir).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(
outcome
.message
.contains("all shippable findings carry a valid ontology type")
);
}
#[test]
fn check_shippable_typing_cmd_blocks_an_untyped_shippable_finding() {
let dir = tempfile::tempdir().unwrap();
let reports_dir = dir.path().join("reports/edu");
fs::create_dir_all(reports_dir.join("findings")).unwrap();
fs::write(
reports_dir.join("findings/f1.json"),
r#"{"@id":"urn:mif:f1","extensions":{"harness":{"verification":{"verdict":"survived"}}}}"#,
)
.unwrap();
fs::write(
reports_dir.join("ontology-map.json"),
r#"[{"finding_id":"urn:mif:f1","valid":true,"basis":"untyped"}]"#,
)
.unwrap();
let outcome = harness_check_shippable_typing_cmd(&reports_dir).unwrap();
assert_eq!(outcome.exit_code, 1);
assert!(outcome.message.contains("synthesis BLOCKED"));
assert!(outcome.message.contains("unblock with"));
}
#[test]
fn check_shippable_typing_cmd_errors_on_a_missing_ontology_map() {
let dir = tempfile::tempdir().unwrap();
let reports_dir = dir.path().join("reports/edu");
fs::create_dir_all(reports_dir.join("findings")).unwrap();
assert!(harness_check_shippable_typing_cmd(&reports_dir).is_err());
}
#[test]
fn falsify_cmd_writes_a_placeholder_inconclusive_verdict_and_logs_the_run_line() {
let dir = tempfile::tempdir().unwrap();
let finding = dir.path().join("f.json");
fs::write(&finding, r#"{"@id":"urn:mif:f1"}"#).unwrap();
let outcome = harness_falsify_cmd(&finding, None).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("\"inconclusive\""));
}
#[test]
fn falsify_cmd_errors_on_a_missing_finding_file() {
let error =
harness_falsify_cmd(std::path::Path::new("/no/such/file.json"), None).unwrap_err();
assert!(matches!(error, mif_rh::MifRhError::Io { .. }));
}
fn write_concordance_fixture(
dir: &std::path::Path,
) -> (std::path::PathBuf, std::path::PathBuf, std::path::PathBuf) {
fs::write(
dir.join("core.yaml"),
"ontology:\n id: mif-generic\n version: \"1.0.0\"\n",
)
.unwrap();
let catalog = dir.join("cat.json");
fs::write(
&catalog,
r#"{"ontologies":[{"id":"mif-generic","version":"1.0.0","source":"core.yaml","core":true}]}"#,
)
.unwrap();
let config = dir.join("cfg.json");
fs::write(&config, r#"{"topics":[]}"#).unwrap();
(catalog, config, dir.to_path_buf())
}
#[test]
fn validate_concordance_cmd_passes_a_conformant_graph() {
let dir = tempfile::tempdir().unwrap();
let (catalog, config, root) = write_concordance_fixture(dir.path());
let concordance = dir.path().join("concordance.json");
fs::write(
&concordance,
r#"{"nodes":[{"id":"n1","entityType":"Concept","topics":[]}],"edges":[]}"#,
)
.unwrap();
let outcome =
harness_validate_concordance_cmd(&concordance, &config, &catalog, &root).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("conformant"));
}
#[test]
fn validate_concordance_cmd_flags_an_undeclared_type() {
let dir = tempfile::tempdir().unwrap();
let (catalog, config, root) = write_concordance_fixture(dir.path());
let concordance = dir.path().join("concordance.json");
fs::write(
&concordance,
r#"{"nodes":[{"id":"n1","entityType":"made-up-type","topics":[]}],"edges":[]}"#,
)
.unwrap();
let outcome =
harness_validate_concordance_cmd(&concordance, &config, &catalog, &root).unwrap();
assert_eq!(outcome.exit_code, 1);
assert!(outcome.message.contains("conformance violation"));
assert!(outcome.message.contains("/ontology-review"));
}
#[test]
fn validate_concordance_cmd_errors_on_a_missing_concordance_file() {
let dir = tempfile::tempdir().unwrap();
let (catalog, config, root) = write_concordance_fixture(dir.path());
let error = harness_validate_concordance_cmd(
&dir.path().join("missing.json"),
&config,
&catalog,
&root,
)
.unwrap_err();
assert!(matches!(error, mif_rh::MifRhError::Io { .. }));
}
#[test]
fn check_ontology_registry_cmd_passes_an_empty_registry() {
let dir = tempfile::tempdir().unwrap();
let outcome = harness_check_ontology_registry_cmd(dir.path());
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("0 type(s)"));
assert!(
outcome
.message
.contains("relationship-endpoint orphans: none")
);
}
#[test]
fn check_ontology_registry_cmd_flags_an_orphaned_relationship_endpoint() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("packs/ontologies/x")).unwrap();
fs::write(
dir.path().join("packs/ontologies/x/x.ontology.yaml"),
"ontology:\n id: x\n version: \"0.1.0\"\nentity_types:\n - name: a\nrelationships:\n rel:\n from: [a]\n to: [does-not-exist]\n",
)
.unwrap();
let outcome = harness_check_ontology_registry_cmd(dir.path());
assert_eq!(outcome.exit_code, 1);
assert!(
outcome
.message
.contains("relationship-endpoint orphans: does-not-exist")
);
}
#[test]
fn check_relationship_targets_cmd_passes_when_every_target_resolves() {
let dir = tempfile::tempdir().unwrap();
let reports_dir = dir.path().join("reports");
fs::create_dir_all(reports_dir.join("t1/findings")).unwrap();
fs::write(
reports_dir.join("t1/findings/a.json"),
r#"{"@id":"urn:mif:t1:a","relationships":[]}"#,
)
.unwrap();
let outcome = harness_check_relationship_targets_cmd(&reports_dir).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("check-relationship-targets: ok"));
}
#[test]
fn check_relationship_targets_cmd_flags_a_dangling_target() {
let dir = tempfile::tempdir().unwrap();
let reports_dir = dir.path().join("reports");
fs::create_dir_all(reports_dir.join("t1/findings")).unwrap();
fs::write(
reports_dir.join("t1/findings/a.json"),
r#"{"@id":"urn:mif:t1:a","relationships":[{"type":"relates-to","target":"urn:mif:t1:missing"}]}"#,
)
.unwrap();
let outcome = harness_check_relationship_targets_cmd(&reports_dir).unwrap();
assert_eq!(outcome.exit_code, 1);
assert!(outcome.message.contains("ORPHAN"));
}
#[test]
fn check_relationship_targets_cmd_errors_on_a_malformed_finding() {
let dir = tempfile::tempdir().unwrap();
let reports_dir = dir.path().join("reports");
fs::create_dir_all(reports_dir.join("t1/findings")).unwrap();
fs::write(reports_dir.join("t1/findings/bad.json"), "{invalid json").unwrap();
assert!(harness_check_relationship_targets_cmd(&reports_dir).is_err());
}
#[test]
fn goal_version_cmd_returns_a_version_id_for_a_wellformed_goal() {
let dir = tempfile::tempdir().unwrap();
let goal_path = dir.path().join("goal.json");
fs::write(
&goal_path,
r#"{"objective":"o","dimensions":["landscape"]}"#,
)
.unwrap();
let outcome = harness_goal_version_cmd(&goal_path).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(!outcome.message.is_empty());
}
#[test]
fn goal_version_cmd_errors_on_a_missing_goal_file() {
let dir = tempfile::tempdir().unwrap();
assert!(harness_goal_version_cmd(&dir.path().join("missing.json")).is_err());
}
fn write_release_fixture(root: &std::path::Path) {
fs::write(
root.join("harness.config.json"),
r#"{"version": "0.4.0", "topics": []}"#,
)
.unwrap();
fs::create_dir_all(root.join(".claude-plugin")).unwrap();
fs::write(
root.join(".claude-plugin/marketplace.json"),
r#"{"name": "research-harness", "metadata": {"version": "0.4.0"}}"#,
)
.unwrap();
fs::write(
root.join("CHANGELOG.md"),
"# Changelog\n\n## [Unreleased]\n\n## [0.4.0] - 2026-01-01\n\nInitial.\n",
)
.unwrap();
}
#[test]
fn bump_version_cmd_bumps_the_release_pointer() {
let dir = tempfile::tempdir().unwrap();
write_release_fixture(dir.path());
let outcome =
harness_bump_version_cmd("patch", &[], Some("2026-02-01"), false, Some(dir.path()))
.unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("0.4.0 -> 0.4.1"));
}
#[test]
fn bump_version_cmd_check_mode_writes_nothing() {
let dir = tempfile::tempdir().unwrap();
write_release_fixture(dir.path());
let outcome =
harness_bump_version_cmd("patch", &[], Some("2026-02-01"), true, Some(dir.path()))
.unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("no files written"));
}
fn git(root: &std::path::Path, args: &[&str]) {
let status = std::process::Command::new("git")
.current_dir(root)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_INDEX_FILE")
.env_remove("GIT_CEILING_DIRECTORIES")
.args(args)
.status()
.unwrap();
assert!(status.success(), "git {args:?} failed");
}
#[test]
fn check_version_bump_cmd_passes_with_no_changes_and_no_prior_release() {
let dir = tempfile::tempdir().unwrap();
git(dir.path(), &["init", "-q", "-b", "main"]);
git(dir.path(), &["config", "user.email", "test@example.com"]);
git(dir.path(), &["config", "user.name", "Test"]);
write_release_fixture(dir.path());
git(dir.path(), &["add", "-A"]);
git(dir.path(), &["commit", "-q", "-m", "base"]);
let outcome = harness_check_version_bump_cmd(Some("HEAD"), Some(dir.path())).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("check-version-bump:"));
}
#[test]
fn project_report_cmd_projects_a_wellformed_report() {
let dir = tempfile::tempdir().unwrap();
let schema = dir.path().join("schema.json");
fs::write(&schema, r#"{"type":"object"}"#).unwrap();
let report = dir.path().join("report.md");
fs::write(&report, "---\nid: r1\n---\n\nBody text.\n").unwrap();
let outcome = harness_project_report_cmd(&report, &schema, &[], None).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(
outcome
.message
.contains("projects to a valid MIF L3 finding")
);
}
#[test]
fn site_toggle_primary_cmd_writes_the_primary_surface() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("harness.config.json");
fs::write(&config, r#"{"version": "1.0.0"}"#).unwrap();
let outcome = harness_site_toggle_primary_cmd("docs", Some(&config)).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("primarySurface -> docs"));
}
#[test]
fn site_toggle_plugin_cmd_flips_the_named_plugin() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("harness.config.json");
fs::write(&config, r#"{"version": "1.0.0"}"#).unwrap();
let outcome = harness_site_toggle_plugin_cmd("llmsTxt", "on", Some(&config)).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("plugin llmsTxt -> enabled=true"));
}
#[test]
fn site_toggle_plugin_cmd_rejects_a_non_on_off_state() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("harness.config.json");
fs::write(&config, r#"{"version": "1.0.0"}"#).unwrap();
assert!(harness_site_toggle_plugin_cmd("llmsTxt", "sideways", Some(&config)).is_err());
}
#[test]
fn pack_toggle_cmd_flips_the_named_packs_flag() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("harness.config.json");
fs::write(
&config,
r#"{"version": "1.0.0", "packs": [{"name": "pdf", "enabled": false}]}"#,
)
.unwrap();
let outcome = harness_pack_toggle_cmd("pdf", "on", Some(&config)).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("pdf -> enabled=true"));
}
#[test]
fn wrap_source_cmd_writes_a_source_envelope() {
let dir = tempfile::tempdir().unwrap();
let schema = dir.path().join("schema.json");
fs::write(&schema, r#"{"type":"object"}"#).unwrap();
let out = dir.path().join("out/source.json");
let outcome = harness_wrap_source_cmd(&WrapSourceArgs {
url: "https://example.com/a",
content_type: "article",
namespace: "edu",
slug: "a",
out: &out,
title: Some("Title"),
content_file: None,
content: Some("some content"),
source_type: None,
schema: &schema,
refs: &[],
})
.unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(out.exists());
assert!(outcome.message.contains("wrap-source: wrote"));
}
#[test]
fn build_graph_cmd_writes_a_graph_from_findings() {
let dir = tempfile::tempdir().unwrap();
let findings_dir = dir.path().join("reports/t1/findings");
fs::create_dir_all(&findings_dir).unwrap();
fs::write(
findings_dir.join("f1.json"),
r#"{"@id":"urn:mif:f1","entity":{"name":"E"}}"#,
)
.unwrap();
let out = dir.path().join("graph.json");
let outcome = harness_build_graph_cmd(&findings_dir, Some(&out)).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(out.exists());
assert!(outcome.message.contains("build-graph: wrote"));
}
#[test]
fn build_index_cmd_writes_an_index_from_findings() {
let dir = tempfile::tempdir().unwrap();
let findings_dir = dir.path().join("reports/t1/findings");
fs::create_dir_all(&findings_dir).unwrap();
fs::write(
findings_dir.join("f1.json"),
r#"{"@id":"urn:mif:f1","entity":{"name":"E"}}"#,
)
.unwrap();
let out = dir.path().join("index.json");
let outcome = harness_build_index_cmd(&findings_dir, Some(&out)).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(out.exists());
assert!(outcome.message.contains("build-index: wrote"));
}
#[test]
fn resolve_membership_cmd_reports_members_stale_and_excluded() {
let dir = tempfile::tempdir().unwrap();
let topic_dir = dir.path().join("reports/t1");
fs::create_dir_all(topic_dir.join("findings")).unwrap();
fs::write(
topic_dir.join("goal.json"),
r#"{"dimensions": ["landscape"]}"#,
)
.unwrap();
fs::write(
dir.path().join("harness.config.json"),
r#"{"freshness": {"default_days": 30}}"#,
)
.unwrap();
fs::write(
topic_dir.join("findings/f1.json"),
r#"{"@id": "urn:mif:f1", "extensions": {"harness": {"dimension": "landscape",
"verification": {"verdict": "survived", "attempted_at": "2026-05-30"}}}}"#,
)
.unwrap();
let outcome = harness_resolve_membership_cmd("t1", Some("gv-1"), Some(dir.path())).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("members: 1"));
}
#[test]
fn graph_stats_cmd_counts_nodes_and_edges() {
let dir = tempfile::tempdir().unwrap();
let graph_path = dir.path().join("knowledge-graph.json");
fs::write(
&graph_path,
r#"{"nodes":[{"id":"a"},{"id":"b"}],"edges":[{"source":"a","target":"b"}]}"#,
)
.unwrap();
let outcome = harness_graph_stats_cmd(&graph_path).unwrap();
assert_eq!(outcome.exit_code, 0);
assert_eq!(outcome.message, "2 1");
}
#[test]
fn render_artifact_cmd_writes_a_rendered_channel_file() {
let dir = tempfile::tempdir().unwrap();
let artifact_path = dir.path().join("artifact.json");
fs::write(
&artifact_path,
r#"{"title":"T","sections":[],"sources":[]}"#,
)
.unwrap();
let out = dir.path().join("out.md");
let outcome = harness_render_artifact_cmd(&RenderArtifactArgs {
artifact: &artifact_path,
channel: "blog",
out: &out,
slug: "t",
slugpath: "reports/t1/t.md",
created: "2026-06-01T00:00:00Z",
version: 1,
verification: None,
})
.unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(out.exists());
assert!(outcome.message.contains("render: wrote"));
}
#[test]
fn synthesize_artifact_cmd_writes_an_artifact_from_findings() {
let dir = tempfile::tempdir().unwrap();
let findings_dir = dir.path().join("reports/t1/findings");
fs::create_dir_all(&findings_dir).unwrap();
fs::write(
findings_dir.join("f1.json"),
r#"{"@id":"urn:mif:f1","content":"insight text",
"citations":[{"url":"https://example.com","title":"Example"}]}"#,
)
.unwrap();
let out = dir.path().join("artifact.json");
let outcome =
harness_synthesize_artifact_cmd(&findings_dir, Some("general"), Some(&out)).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(out.exists());
assert!(outcome.message.contains("synthesize: wrote"));
}
#[test]
fn import_corpus_cmd_imports_findings_into_a_topic() {
let dir = tempfile::tempdir().unwrap();
let schema = dir.path().join("schema.json");
fs::write(&schema, r#"{"type":"object"}"#).unwrap();
let src = dir.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(
src.join("f1.json"),
r#"{"@id":"urn:mif:f1","provenance":{"sourceType":"web"}}"#,
)
.unwrap();
let dest = dir.path().join("reports/t1");
let outcome = harness_import_corpus_cmd(&ImportCorpusArgs {
src_findings_dir: &src,
dest_dir: &dest,
topic: "t1",
schema: &schema,
refs: &[],
config: None,
})
.unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("import: imported"));
}
#[test]
fn build_concordance_cmd_writes_a_concordance_across_topics() {
let dir = tempfile::tempdir().unwrap();
let reports_dir = dir.path().join("reports");
fs::create_dir_all(reports_dir.join("t1/findings")).unwrap();
fs::write(
reports_dir.join("t1/findings/f1.json"),
r#"{"@id":"urn:mif:f1","entity":{"name":"E","entity_type":"concept"}}"#,
)
.unwrap();
let out = dir.path().join("concordance.json");
let outcome = harness_build_concordance_cmd(&reports_dir, Some(&out)).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(out.exists());
assert!(outcome.message.contains("build-concordance: wrote"));
}
#[test]
fn reconcile_session_cmd_reports_nothing_to_do_on_a_complete_session() {
let dir = tempfile::tempdir().unwrap();
let schema = dir.path().join("schema.json");
fs::write(&schema, r#"{"type":"object"}"#).unwrap();
let topic_dir = dir.path().join("reports/t1");
fs::create_dir_all(topic_dir.join("findings")).unwrap();
let sample = dir.path().join("sample.json");
fs::write(&sample, r#"{"@id":"urn:mif:sample"}"#).unwrap();
let outcome = harness_reconcile_session_cmd(&topic_dir, &schema, &[], &sample).unwrap();
assert_eq!(outcome.exit_code, 0);
}
#[test]
fn topic_metadata_cmd_prints_a_shell_script_for_a_registered_topic() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("harness.config.json");
fs::write(
&config,
r#"{"topics": [{"id": "t1", "title": "Topic One", "status": "active"}],
"dimensions": [{"id": "landscape", "description": "Market landscape"}]}"#,
)
.unwrap();
let findings_dir = dir.path().join("findings");
fs::create_dir_all(&findings_dir).unwrap();
let goal_path = dir.path().join("goal.json");
let outcome = harness_topic_metadata_cmd("t1", &config, &findings_dir, &goal_path).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(!outcome.message.is_empty());
}
#[test]
fn topic_metadata_cmd_errors_on_an_unregistered_topic() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("harness.config.json");
fs::write(&config, r#"{"topics": []}"#).unwrap();
let findings_dir = dir.path().join("findings");
fs::create_dir_all(&findings_dir).unwrap();
assert!(
harness_topic_metadata_cmd(
"nope",
&config,
&findings_dir,
&dir.path().join("goal.json")
)
.is_err()
);
}
#[test]
fn synthesize_corpus_cmd_writes_a_map_and_markdown() {
let dir = tempfile::tempdir().unwrap();
let concordance = dir.path().join("concordance.json");
fs::write(
&concordance,
r#"{"nodes":[{"id":"n1","entityType":"Concept","topics":["t1"],"name":"N1"}],"edges":[]}"#,
)
.unwrap();
let map_out = dir.path().join("map.json");
let markdown_out = dir.path().join("out.md");
let outcome =
harness_synthesize_corpus_cmd(&concordance, &map_out, &markdown_out, None).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(map_out.exists());
assert!(markdown_out.exists());
assert!(outcome.message.contains("synthesize-corpus: wrote"));
}
fn write_ontology_root(root: &std::path::Path) {
fs::create_dir_all(root.join(".claude")).unwrap();
fs::write(
root.join("harness.config.json"),
r#"{"topics":[{"id":"edu","ontologies":["edu-fixture"]}],"ontologies":[]}"#,
)
.unwrap();
fs::write(
root.join(".claude/enabled-packs.json"),
r#"{"ontologies":[]}"#,
)
.unwrap();
}
#[test]
fn ontology_fetch_cmd_reports_nothing_to_fetch_with_no_ids() {
let dir = tempfile::tempdir().unwrap();
write_ontology_root(dir.path());
let outcome = ontology_fetch_cmd(&[], false, Some(dir.path()), None, None).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("nothing to fetch"));
}
#[test]
fn ontology_sync_cmd_catalogs_the_core_layers() {
let dir = tempfile::tempdir().unwrap();
write_ontology_root(dir.path());
let config = dir.path().join("harness.config.json");
let catalog = dir.path().join(".claude/enabled-packs.json");
let outcome = ontology_sync_cmd(Some(dir.path()), Some(&config), Some(&catalog)).unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("ontology sync: cataloged"));
}
#[test]
fn ontology_lock_check_cmd_passes_with_no_lock_file() {
let dir = tempfile::tempdir().unwrap();
write_ontology_root(dir.path());
let config = dir.path().join("harness.config.json");
let outcome = ontology_lock_check_cmd(Some(dir.path()), Some(&config)).unwrap();
assert_eq!(outcome.exit_code, 0);
}
#[test]
fn ontology_sync_registry_cmd_reports_no_new_ontologies_against_a_local_registry() {
let dir = tempfile::tempdir().unwrap();
write_ontology_root(dir.path());
let config = dir.path().join("harness.config.json");
let catalog = dir.path().join(".claude/enabled-packs.json");
let registry = dir.path().join("registry");
fs::create_dir_all(®istry).unwrap();
fs::write(registry.join("index.json"), r#"{"ontologies":{}}"#).unwrap();
let outcome = ontology_sync_registry_cmd(
Some(dir.path()),
Some(&config),
Some(&catalog),
Some(®istry.display().to_string()),
)
.unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(outcome.message.contains("no new ontologies"));
}
#[test]
fn ontology_author_cmd_drafts_from_a_topic_map() {
let dir = tempfile::tempdir().unwrap();
let reports_dir = dir.path().join("reports");
fs::create_dir_all(reports_dir.join("t1")).unwrap();
fs::write(
reports_dir.join("t1/ontology-map.json"),
r#"[{"finding_id":"urn:mif:f1","valid":false,"basis":"untyped",
"entity_type":"widget","resolved_ontology":"mif-generic@1.0.0"}]"#,
)
.unwrap();
let out = dir.path().join("new-id.ontology.yaml");
let outcome =
ontology_author_cmd("new-id", Some("t1"), None, Some(&out), Some(&reports_dir))
.unwrap();
assert_eq!(outcome.exit_code, 0);
assert!(out.exists());
assert!(outcome.message.contains("ontology author: drafted"));
}
}