use std::collections::HashMap;
use std::io::Read;
use anyhow::{Context, Result};
use camino::Utf8PathBuf;
use cordance_core::lock::SourceLock;
use cordance_core::pack::{CordancePack, DoctrinePin, PackTargets, ProjectIdentity};
use sha2::{Digest, Sha256};
pub struct PackConfig {
pub target: Utf8PathBuf,
pub output_mode: OutputMode,
pub selected_targets: PackTargets,
pub doctrine_root: Option<Utf8PathBuf>,
pub llm_provider: Option<String>,
pub ollama_model: Option<String>,
pub quiet: bool,
pub from_cortex_push: bool,
pub cortex_receipt_requested_explicitly: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
Write,
DryRun,
Diff,
}
impl OutputMode {
pub fn from_str(s: &str) -> Self {
match s {
"dry-run" => Self::DryRun,
"diff" => Self::Diff,
_ => Self::Write,
}
}
}
#[allow(clippy::too_many_lines)]
pub fn run(config: &PackConfig) -> Result<CordancePack> {
let cfg = crate::config::Config::load_strict(&config.target)
.with_context(|| format!("loading cordance.toml at {}", config.target))?;
precheck_pack_metadata_destinations(config, &cfg)?;
let name = detect_project_name(&config.target);
let kind = detect_project_kind(&config.target);
let host_os = std::env::consts::OS.to_string();
let axiom_pin = Some(cfg.axiom_version(&config.target));
let (sources, blocked_count, scan_error) = match cordance_scan::scan_repo(&config.target) {
Ok(all) => {
let total = all.len();
let unblocked: Vec<_> = all.into_iter().filter(|r| !r.blocked).collect();
let blocked_n = total - unblocked.len();
if blocked_n > 0 {
tracing::debug!(
blocked = blocked_n,
total,
"cordance pack: dropped blocked entries from pack.sources"
);
}
(unblocked, blocked_n, None)
}
Err(e) => {
tracing::warn!(error = %e, "cordance pack: source scan failed");
(vec![], 0, Some(e.to_string()))
}
};
let _ = blocked_count;
let doctrine_source_str = config
.doctrine_root
.as_ref()
.map_or_else(|| cfg.doctrine.source.clone(), |p| p.as_str().to_string());
let doctrine_root = config
.doctrine_root
.clone()
.unwrap_or_else(|| cfg.doctrine_root(&config.target));
let pin_commit: Option<&str> = if cfg.doctrine.pin_commit == "auto" {
None
} else {
Some(cfg.doctrine.pin_commit.as_str())
};
let mut doctrine_load_warning: Option<String> = None;
let doctrine_cache_dir =
cordance_core::paths::doctrine_cache_dir_for_url(&cfg.doctrine.fallback_repo);
let doctrine_idx = match cordance_doctrine::load_doctrine_with_fallback(
&doctrine_root,
&cfg.doctrine.fallback_repo,
Some(&doctrine_cache_dir),
pin_commit,
) {
Ok(idx) => idx,
Err(err) => {
tracing::warn!(error = %err, "doctrine load failed; using empty index");
doctrine_load_warning = Some(format!("doctrine load failed: {err}"));
cordance_doctrine::DoctrineIndex::empty(doctrine_root.clone())
}
};
let doctrine_pins: Vec<DoctrinePin> = if let Some(commit) = &doctrine_idx.commit {
vec![DoctrinePin {
repo: doctrine_idx.repo.clone(),
commit: commit.clone(),
source_path: Utf8PathBuf::from(
format!("{doctrine_source_str}/doctrine/SEMANTIC_INDEX.md").replace('\\', "/"),
),
}]
} else {
vec![]
};
let mut pack = CordancePack {
schema: cordance_core::schema::CORDANCE_PACK_V1.into(),
project: ProjectIdentity {
name,
repo_root: Utf8PathBuf::from("."),
kind,
host_os,
axiom_pin,
},
sources,
doctrine_pins,
targets: config.selected_targets.clone(),
outputs: vec![],
source_lock: SourceLock::empty(),
advise: cordance_core::advise::AdviseReport::empty(),
residual_risk: vec!["claim_ceiling=candidate".into()],
};
if let Some(err_msg) = scan_error {
pack.residual_risk.push(format!("scan error: {err_msg}"));
}
if config.selected_targets.cortex_receipt
&& config.cortex_receipt_requested_explicitly
&& !config.from_cortex_push
{
pack.residual_risk.push(
"cortex-receipt requested via --targets but pack emits no receipt; \
use `cordance cortex push` to produce the receipt"
.into(),
);
}
if let Some(warning) = doctrine_load_warning {
pack.residual_risk.push(warning);
}
maybe_run_llm(config, &cfg, &mut pack);
let emitters = build_emitters(&config.selected_targets);
let all_outputs = dispatch_emitters(&emitters, config, &pack)?;
pack.outputs = all_outputs;
pack.source_lock = SourceLock::compute_from_pack(&pack);
write_sources_lock(config, &pack)?;
if let Ok(report) = cordance_advise::run_all(&pack) {
pack.advise = report;
}
let post_emit_emitters: Vec<Box<dyn cordance_emit::TargetEmitter>> = vec![
Box::new(cordance_emit::pack_json::PackJsonEmitter),
Box::new(cordance_emit::evidence_map::EvidenceMapEmitter),
];
let _post_outputs = dispatch_emitters(&post_emit_emitters, config, &pack)?;
Ok(pack)
}
fn write_sources_lock(config: &PackConfig, pack: &CordancePack) -> Result<()> {
if config.output_mode != OutputMode::Write {
return Ok(());
}
let lock_path = config.target.join(".cordance").join("sources.lock");
let lock_json =
serde_json::to_string_pretty(&pack.source_lock).context("serialising sources.lock")?;
cordance_core::fs::safe_write_with_mkdir(lock_path.as_std_path(), lock_json.as_bytes())
.context("writing sources.lock")?;
Ok(())
}
fn precheck_pack_metadata_destinations(
config: &PackConfig,
cfg: &crate::config::Config,
) -> Result<()> {
if config.output_mode != OutputMode::Write {
return Ok(());
}
let mut destinations = vec![
".cordance/sources.lock",
".cordance/pack.json",
".cordance/evidence-map.json",
];
if resolve_llm_provider(config, cfg).is_some() {
destinations.push(".cordance/llm-candidate.json");
}
for rel_path in destinations {
let abs_path = config.target.join(rel_path);
precheck_no_reparse_destination(&abs_path, rel_path, "pack metadata")?;
}
Ok(())
}
fn build_emitters(targets: &PackTargets) -> Vec<Box<dyn cordance_emit::TargetEmitter>> {
let mut v: Vec<Box<dyn cordance_emit::TargetEmitter>> = vec![];
if targets.claude_code {
v.push(Box::new(cordance_emit::agents_md::AgentsMdEmitter));
v.push(Box::new(cordance_emit::claude_md::ClaudeMdEmitter));
}
if targets.cursor {
v.push(Box::new(cordance_emit::cursor::CursorEmitter));
}
if targets.codex {
v.push(Box::new(cordance_emit::codex::CodexEmitter));
}
if targets.axiom_harness_target {
v.push(Box::new(
cordance_emit::harness_target::HarnessTargetEmitter,
));
}
let _ = targets.cortex_receipt;
v
}
type RenderBatch = Vec<(&'static str, Vec<(Utf8PathBuf, Vec<u8>)>)>;
fn dispatch_emitters(
emitters: &[Box<dyn cordance_emit::TargetEmitter>],
config: &PackConfig,
pack: &CordancePack,
) -> Result<Vec<cordance_core::pack::PackOutput>> {
match config.output_mode {
OutputMode::Write => dispatch_emitters_write(emitters, config, pack),
OutputMode::DryRun => dispatch_emitters_dry_run(emitters, config, pack),
OutputMode::Diff => dispatch_emitters_diff(emitters, config, pack),
}
}
fn dispatch_emitters_write(
emitters: &[Box<dyn cordance_emit::TargetEmitter>],
config: &PackConfig,
pack: &CordancePack,
) -> Result<Vec<cordance_core::pack::PackOutput>> {
let rendered_per_emitter = render_all(emitters, pack)?;
precheck_all_destinations(&rendered_per_emitter, &config.target)?;
commit_all(emitters, config, pack)
}
fn render_all(
emitters: &[Box<dyn cordance_emit::TargetEmitter>],
pack: &CordancePack,
) -> Result<RenderBatch> {
let mut rendered_per_emitter: RenderBatch = Vec::with_capacity(emitters.len());
for emitter in emitters {
let rendered = emitter
.render(pack)
.with_context(|| format!("emitter '{}' render failed", emitter.name()))?;
rendered_per_emitter.push((emitter.name(), rendered));
}
Ok(rendered_per_emitter)
}
fn precheck_all_destinations(
rendered_per_emitter: &RenderBatch,
target: &Utf8PathBuf,
) -> Result<()> {
for (emitter_name, rendered) in rendered_per_emitter {
for (rel_path, _bytes) in rendered {
let abs_path = target.join(rel_path);
let owner = format!("emitter '{emitter_name}'");
precheck_no_reparse_destination(&abs_path, rel_path.as_str(), &owner)?;
}
}
Ok(())
}
fn precheck_no_reparse_destination(
abs_path: &Utf8PathBuf,
rel_path: &str,
owner: &str,
) -> Result<()> {
if let Err(io_err) =
cordance_core::fs::precheck_no_reparse_point_ancestor(abs_path.as_std_path())
{
let refusal_path = io_err
.get_ref()
.and_then(|e| e.downcast_ref::<cordance_core::fs::SymlinkRefusal>())
.map(|r| r.path.display().to_string());
if let Some(p) = refusal_path {
return Err(anyhow::anyhow!(
"{owner} refused before write: \
refusing to write through symlink/reparse-point at {p} \
(destination: {rel_path})"
));
}
return Err(
anyhow::Error::new(io_err).context(format!("{owner} pre-check failed for {rel_path}"))
);
}
Ok(())
}
fn commit_all(
emitters: &[Box<dyn cordance_emit::TargetEmitter>],
config: &PackConfig,
pack: &CordancePack,
) -> Result<Vec<cordance_core::pack::PackOutput>> {
let mut all_outputs = Vec::new();
let mut succeeded: Vec<String> = Vec::with_capacity(emitters.len());
for emitter in emitters {
match emitter.emit(pack, &config.target) {
Ok(outputs) => {
succeeded.push(emitter.name().to_string());
all_outputs.extend(outputs);
}
Err(err) => {
if !succeeded.is_empty() {
eprintln!(
"cordance: commit-phase failure after {} emitter(s) wrote: {}",
succeeded.len(),
succeeded.join(", ")
);
}
if let cordance_emit::EmitError::Io(io_err) = &err {
if let Some(refusal) = io_err
.get_ref()
.and_then(|e| e.downcast_ref::<cordance_core::fs::SymlinkRefusal>())
{
let emitter_name = emitter.name();
let path_display = refusal.path.display();
return Err(anyhow::anyhow!(
"emitter '{emitter_name}' failed at commit phase: \
refusing to write through symlink/reparse-point at {path_display}"
));
}
}
let emitter_name = emitter.name().to_string();
return Err(anyhow::Error::new(err)
.context(format!("emitter '{emitter_name}' failed at commit phase")));
}
}
}
Ok(all_outputs)
}
fn dispatch_emitters_dry_run(
emitters: &[Box<dyn cordance_emit::TargetEmitter>],
config: &PackConfig,
pack: &CordancePack,
) -> Result<Vec<cordance_core::pack::PackOutput>> {
let mut all_outputs = Vec::new();
for emitter in emitters {
let outputs = emitter
.plan(pack, &config.target)
.with_context(|| format!("emitter '{}' plan failed", emitter.name()))?;
if !config.quiet {
for o in &outputs {
println!("would write: {} ({} bytes)", o.path, o.bytes);
}
}
all_outputs.extend(outputs);
}
Ok(all_outputs)
}
fn dispatch_emitters_diff(
emitters: &[Box<dyn cordance_emit::TargetEmitter>],
config: &PackConfig,
pack: &CordancePack,
) -> Result<Vec<cordance_core::pack::PackOutput>> {
let mut all_outputs = Vec::new();
for emitter in emitters {
let outputs = emitter
.plan(pack, &config.target)
.with_context(|| format!("emitter '{}' diff-plan failed", emitter.name()))?;
for o in &outputs {
let abs = config.target.join(&o.path);
let line = if abs.exists() {
let on_disk = std::fs::read_to_string(&abs).unwrap_or_default();
let on_disk_sha = hex::encode(Sha256::digest(on_disk.as_bytes()));
if on_disk_sha == o.sha256 {
format!("unchanged: {}", o.path)
} else {
format!("changed: {}", o.path)
}
} else {
format!("new: {}", o.path)
};
if !config.quiet {
println!("{line}");
}
all_outputs.push(o.clone());
}
}
Ok(all_outputs)
}
fn detect_project_name(root: &Utf8PathBuf) -> String {
if let Some(name) = read_cargo_package_name(root) {
return name;
}
if let Ok(canonical) = std::fs::canonicalize(root.as_std_path()) {
if let Some(dir_name) = canonical.file_name() {
if let Some(s) = dir_name.to_str() {
return s.to_string();
}
}
}
root.file_name().unwrap_or("unknown").to_string()
}
fn read_cargo_package_name(root: &Utf8PathBuf) -> Option<String> {
#[derive(serde::Deserialize, Default)]
struct PkgSection {
name: Option<String>,
}
#[derive(serde::Deserialize, Default)]
struct WorkspaceSection {
package: Option<PkgSection>,
}
#[derive(serde::Deserialize, Default)]
struct CargoToml {
package: Option<PkgSection>,
workspace: Option<WorkspaceSection>,
}
let cargo = root.join("Cargo.toml");
if !cargo.exists() {
return None;
}
let content = std::fs::read_to_string(&cargo).ok()?;
let parsed: CargoToml = toml::from_str(&content).ok()?;
if let Some(pkg) = parsed.package {
if let Some(name) = pkg.name {
if !name.is_empty() {
return Some(name);
}
}
}
if let Some(ws) = parsed.workspace {
if let Some(pkg) = ws.package {
if let Some(name) = pkg.name {
if !name.is_empty() {
return Some(name);
}
}
}
}
None
}
fn detect_project_kind(root: &Utf8PathBuf) -> String {
if root.join("Cargo.toml").exists() {
return "rust-workspace".into();
}
if root.join("package.json").exists() {
return "typescript-node".into();
}
if root.join("pyproject.toml").exists() || root.join("setup.py").exists() {
return "python".into();
}
if root.join("go.mod").exists() {
return "go".into();
}
"unknown".into()
}
fn resolve_llm_provider(config: &PackConfig, cfg: &crate::config::Config) -> Option<String> {
config
.llm_provider
.clone()
.or_else(|| Some(cfg.llm.provider.clone()))
.filter(|p| p != "none")
}
fn maybe_run_llm(config: &PackConfig, cfg: &crate::config::Config, pack: &mut CordancePack) {
let Some(provider) = resolve_llm_provider(config, cfg) else {
return;
};
if provider != "ollama" {
pack.residual_risk.push(format!(
"LLM provider '{provider}' is not supported; skipping enrichment"
));
return;
}
let mut adapter = cordance_llm::OllamaAdapter::from_config(&cfg.llm.ollama);
if let Some(override_model) = &config.ollama_model {
override_model.clone_into(&mut adapter.model);
}
let base_url = adapter.base_url.clone();
let model = adapter.model.clone();
if !adapter.is_available() {
eprintln!(
"cordance: warning — Ollama not reachable at {base_url}, skipping LLM enrichment"
);
pack.residual_risk.push(format!(
"LLM enrichment skipped: ollama/{model} at {base_url} unreachable"
));
return;
}
let sources: Vec<_> = pack
.sources
.iter()
.filter(|s| !s.blocked)
.take(20)
.cloned()
.collect();
let source_ids: Vec<String> = sources.iter().map(|s| s.id.clone()).collect();
let prompt = cordance_llm::prompt::bounded_pack_summary_prompt(
&sources,
&[],
"Summarise this project's main purpose in one paragraph as a candidate observation.",
);
let source_content_map = load_source_content_map(pack, &source_ids);
match adapter.generate_with_grounding(&prompt, &source_ids, &source_content_map) {
Ok(candidate) => {
let claim_count = candidate.claims.len();
pack.residual_risk.push(format!(
"LLM candidate prose attached: {claim_count} claim(s) from ollama/{model}"
));
let candidate_path = config.target.join(".cordance").join("llm-candidate.json");
match serde_json::to_string_pretty(&candidate) {
Ok(json) => {
if let Err(e) = cordance_core::fs::safe_write_with_mkdir(
candidate_path.as_std_path(),
json.as_bytes(),
) {
eprintln!("cordance: warning — failed to write {candidate_path}: {e}");
}
}
Err(e) => {
eprintln!("cordance: warning — failed to serialise LLM candidate: {e}");
}
}
}
Err(e) => {
eprintln!("cordance: warning — Ollama generation failed: {e}");
pack.residual_risk
.push(format!("LLM enrichment failed: {e}"));
}
}
}
fn load_source_content_map(pack: &CordancePack, cited_ids: &[String]) -> HashMap<String, String> {
const MAX_PER_SOURCE: u64 = 4 * 1024;
let mut map: HashMap<String, String> = HashMap::with_capacity(cited_ids.len());
for id in cited_ids {
let Some(record) = pack.sources.iter().find(|s| &s.id == id) else {
continue;
};
if record.blocked {
continue;
}
let abs = pack.project.repo_root.join(&record.path);
let Ok(file) = std::fs::File::open(abs.as_std_path()) else {
continue;
};
let mut buf = String::new();
if file.take(MAX_PER_SOURCE).read_to_string(&mut buf).is_ok() {
map.insert(id.clone(), buf);
}
}
map
}
#[cfg(test)]
mod tests {
use super::*;
fn write_cargo(content: &str) -> (tempfile::TempDir, Utf8PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
let target = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).expect("tempdir is utf8");
std::fs::write(target.join("Cargo.toml"), content).expect("write Cargo.toml");
(dir, target)
}
fn temp_utf8_dir() -> (tempfile::TempDir, Utf8PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
let target = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).expect("tempdir is utf8");
(dir, target)
}
fn config_for_test(
target: &Utf8PathBuf,
output_mode: OutputMode,
selected_targets: PackTargets,
doctrine_root: &Utf8PathBuf,
from_cortex_push: bool,
cortex_receipt_requested_explicitly: bool,
) -> PackConfig {
PackConfig {
target: target.clone(),
output_mode,
selected_targets,
doctrine_root: Some(doctrine_root.clone()),
llm_provider: Some("none".to_string()),
ollama_model: None,
quiet: true,
from_cortex_push,
cortex_receipt_requested_explicitly,
}
}
fn cortex_receipt_noop_note() -> &'static str {
"cortex-receipt requested via --targets but pack emits no receipt"
}
#[test]
fn detect_project_name_reads_package_section() {
let (_d, target) = write_cargo("[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\n");
assert_eq!(detect_project_name(&target), "my-crate");
}
#[test]
fn detect_project_name_handles_no_space_around_equals() {
let (_d, target) = write_cargo("[package]\nname=\"compact\"\n");
assert_eq!(detect_project_name(&target), "compact");
}
#[test]
fn detect_project_name_reads_workspace_package_section() {
let (_d, target) = write_cargo(
"[workspace]\nmembers = [\"foo\"]\n\n[workspace.package]\nname = \"ws-name\"\nversion = \"0.1.0\"\n",
);
assert_eq!(detect_project_name(&target), "ws-name");
}
#[test]
fn detect_project_name_prefers_package_over_workspace_package() {
let (_d, target) = write_cargo(
"[package]\nname = \"actual\"\n\n[workspace.package]\nname = \"inherited\"\n",
);
assert_eq!(detect_project_name(&target), "actual");
}
#[test]
fn detect_project_name_ignores_bin_name() {
let (_d, target) = write_cargo(
"[package]\nname = \"libname\"\n\n[[bin]]\nname = \"binname\"\npath = \"src/main.rs\"\n",
);
assert_eq!(detect_project_name(&target), "libname");
}
#[test]
fn detect_project_name_falls_back_when_cargo_toml_absent() {
let dir = tempfile::tempdir().expect("tempdir");
let target = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).expect("tempdir is utf8");
let detected = detect_project_name(&target);
let expected = target.file_name().unwrap_or("unknown").to_string();
assert!(!detected.is_empty());
assert!(
detected == expected
|| std::fs::canonicalize(target.as_std_path())
.ok()
.and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string()))
.is_some_and(|c| detected == c),
"detected={detected} expected={expected}"
);
}
#[test]
fn detect_project_name_falls_back_when_cargo_toml_malformed() {
let (_d, target) = write_cargo("this is not = = valid toml");
let detected = detect_project_name(&target);
assert!(!detected.is_empty(), "must produce some non-empty fallback");
}
#[test]
fn detect_project_name_falls_back_when_name_missing() {
let (_d, target) = write_cargo("[package]\nversion = \"0.1.0\"\n");
let detected = detect_project_name(&target);
assert!(!detected.is_empty());
assert_ne!(detected, "0.1.0");
}
#[test]
fn write_sources_lock_propagates_io_failure_instead_of_swallowing() {
let dir = tempfile::tempdir().expect("tempdir");
let target = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).expect("tempdir is utf8");
std::fs::write(target.join(".cordance"), b"blocker").expect("seed blocker file");
let config = PackConfig {
target: target.clone(),
output_mode: OutputMode::Write,
selected_targets: PackTargets::default(),
doctrine_root: None,
llm_provider: None,
ollama_model: None,
quiet: true,
from_cortex_push: false,
cortex_receipt_requested_explicitly: false,
};
let pack = CordancePack {
schema: "test".into(),
project: ProjectIdentity {
name: "test".into(),
kind: "unknown".into(),
repo_root: target,
host_os: "test".into(),
axiom_pin: None,
},
sources: vec![],
doctrine_pins: vec![],
targets: PackTargets::default(),
outputs: vec![],
source_lock: SourceLock::empty(),
advise: cordance_core::advise::AdviseReport::empty(),
residual_risk: vec![],
};
let result = write_sources_lock(&config, &pack);
assert!(
result.is_err(),
"expected write_sources_lock to surface the create_dir_all I/O failure, got Ok"
);
let err = result.unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("writing sources.lock"),
"error chain must mention `writing sources.lock`; got: {msg}"
);
}
#[test]
fn cortex_push_pack_config_omits_cortex_receipt_noop_audit_note() {
let (_target_dir, target) = temp_utf8_dir();
let (_doctrine_dir, doctrine_root) = temp_utf8_dir();
let config = config_for_test(
&target,
OutputMode::DryRun,
PackTargets {
cortex_receipt: true,
..Default::default()
},
&doctrine_root,
true,
true,
);
let pack = run(&config).expect("cortex-push pack run should succeed");
assert!(
!pack
.residual_risk
.iter()
.any(|risk| risk.contains(cortex_receipt_noop_note())),
"cordance cortex push must not pollute receipt claim language via residual_risk: {:?}",
pack.residual_risk
);
}
#[test]
fn pack_cli_multi_target_cortex_receipt_records_noop_audit_note() {
let (_target_dir, target) = temp_utf8_dir();
let (_doctrine_dir, doctrine_root) = temp_utf8_dir();
let config = config_for_test(
&target,
OutputMode::DryRun,
PackTargets {
axiom_harness_target: true,
cortex_receipt: true,
..Default::default()
},
&doctrine_root,
false,
true,
);
let pack = run(&config).expect("pack run should succeed");
assert!(
pack.residual_risk
.iter()
.any(|risk| risk.contains(cortex_receipt_noop_note())),
"cordance pack with cortex-receipt in any target set must record no-op audit note; got {:?}",
pack.residual_risk
);
}
#[test]
fn default_all_targets_omits_cortex_receipt_noop_audit_note() {
let (_target_dir, target) = temp_utf8_dir();
let (_doctrine_dir, doctrine_root) = temp_utf8_dir();
let config = config_for_test(
&target,
OutputMode::DryRun,
PackTargets::all(),
&doctrine_root,
false,
false,
);
let pack = run(&config).expect("default all-target pack run should succeed");
assert!(
!pack
.residual_risk
.iter()
.any(|risk| risk.contains(cortex_receipt_noop_note())),
"default all-target pack must not pretend --targets cortex-receipt was requested: {:?}",
pack.residual_risk
);
}
#[cfg(unix)]
struct FakeEmitter {
name: &'static str,
outputs: Vec<(Utf8PathBuf, Vec<u8>)>,
}
#[cfg(unix)]
impl cordance_emit::TargetEmitter for FakeEmitter {
fn name(&self) -> &'static str {
self.name
}
fn render(
&self,
_pack: &CordancePack,
) -> Result<Vec<(Utf8PathBuf, Vec<u8>)>, cordance_emit::EmitError> {
Ok(self.outputs.clone())
}
}
#[cfg(unix)]
fn empty_pack(target: Utf8PathBuf) -> CordancePack {
CordancePack {
schema: "test".into(),
project: ProjectIdentity {
name: "test".into(),
kind: "unknown".into(),
repo_root: target,
host_os: "test".into(),
axiom_pin: None,
},
sources: vec![],
doctrine_pins: vec![],
targets: PackTargets::default(),
outputs: vec![],
source_lock: SourceLock::empty(),
advise: cordance_core::advise::AdviseReport::empty(),
residual_risk: vec![],
}
}
#[cfg(unix)]
#[test]
fn pre_check_fails_with_planted_symlink_ancestor_before_any_write() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().expect("tempdir");
let target = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).expect("tempdir is utf8");
let escape = tempfile::tempdir().expect("escape tempdir");
let dotcordance = target.as_std_path().join(".cordance");
symlink(escape.path(), &dotcordance).expect("plant .cordance symlink");
let emitters: Vec<Box<dyn cordance_emit::TargetEmitter>> = vec![
Box::new(FakeEmitter {
name: "fake:clean",
outputs: vec![(Utf8PathBuf::from("AGENTS.md"), b"clean-bytes".to_vec())],
}),
Box::new(FakeEmitter {
name: "fake:hostile",
outputs: vec![(
Utf8PathBuf::from(".cordance/pack.json"),
b"would-write-through-junction".to_vec(),
)],
}),
];
let config = PackConfig {
target: target.clone(),
output_mode: OutputMode::Write,
selected_targets: PackTargets::default(),
doctrine_root: None,
llm_provider: None,
ollama_model: None,
quiet: true,
from_cortex_push: false,
cortex_receipt_requested_explicitly: false,
};
let pack = empty_pack(target.clone());
let result = dispatch_emitters(&emitters, &config, &pack);
let err = result.expect_err("planted symlink ancestor must fail dispatch");
let msg = format!("{err:#}");
assert!(
msg.contains("symlink") || msg.contains("reparse-point"),
"error must indicate refusal class; got: {msg}"
);
assert!(
msg.contains(".cordance"),
"error must name the offending ancestor path; got: {msg}"
);
let agents_md = target.as_std_path().join("AGENTS.md");
assert!(
!agents_md.exists(),
"Phase-2 pre-check must fail BEFORE any emitter writes; \
AGENTS.md must not be on disk"
);
let escape_entries: Vec<_> = std::fs::read_dir(escape.path())
.expect("read escape")
.collect();
assert!(
escape_entries.is_empty(),
"no bytes may have leaked through the symlink; escape tree \
must be empty, found: {escape_entries:?}"
);
}
#[cfg(unix)]
#[test]
fn dispatch_error_preserves_symlink_refusal_ancestor_path() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().expect("tempdir");
let target = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).expect("tempdir is utf8");
let escape = tempfile::tempdir().expect("escape tempdir");
let agents_md = target.as_std_path().join("AGENTS.md");
let dest = escape.path().join("operator.txt");
std::fs::write(&dest, b"operator owned").expect("seed");
symlink(&dest, &agents_md).expect("plant AGENTS.md symlink");
let emitters: Vec<Box<dyn cordance_emit::TargetEmitter>> = vec![Box::new(FakeEmitter {
name: "fake:hostile",
outputs: vec![(Utf8PathBuf::from("AGENTS.md"), b"attacker-bytes".to_vec())],
})];
let config = PackConfig {
target: target.clone(),
output_mode: OutputMode::Write,
selected_targets: PackTargets::default(),
doctrine_root: None,
llm_provider: None,
ollama_model: None,
quiet: true,
from_cortex_push: false,
cortex_receipt_requested_explicitly: false,
};
let pack = empty_pack(target.clone());
let err = dispatch_emitters(&emitters, &config, &pack)
.expect_err("symlinked leaf must be refused");
let msg = format!("{err:#}");
assert!(
msg.contains("symlink") || msg.contains("reparse-point"),
"error must indicate refusal class (round-8 redteam #4); got: {msg}"
);
assert!(
msg.contains("AGENTS.md"),
"error must name the offending leaf path; got: {msg}"
);
assert_eq!(
std::fs::read(&dest).expect("read operator file"),
b"operator owned",
"symlink target must not be overwritten"
);
}
#[cfg(windows)]
struct FakeEmitter {
name: &'static str,
outputs: Vec<(Utf8PathBuf, Vec<u8>)>,
}
#[cfg(windows)]
impl cordance_emit::TargetEmitter for FakeEmitter {
fn name(&self) -> &'static str {
self.name
}
fn render(
&self,
_pack: &CordancePack,
) -> Result<Vec<(Utf8PathBuf, Vec<u8>)>, cordance_emit::EmitError> {
Ok(self.outputs.clone())
}
}
#[cfg(windows)]
fn empty_pack(target: Utf8PathBuf) -> CordancePack {
CordancePack {
schema: "test".into(),
project: ProjectIdentity {
name: "test".into(),
kind: "unknown".into(),
repo_root: target,
host_os: "test".into(),
axiom_pin: None,
},
sources: vec![],
doctrine_pins: vec![],
targets: PackTargets::default(),
outputs: vec![],
source_lock: SourceLock::empty(),
advise: cordance_core::advise::AdviseReport::empty(),
residual_risk: vec![],
}
}
#[cfg(windows)]
#[test]
fn pre_check_fails_with_planted_junction_ancestor_before_any_write_windows() {
use std::process::Command;
let dir = tempfile::tempdir().expect("tempdir");
let target = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).expect("tempdir is utf8");
let escape = tempfile::tempdir().expect("escape tempdir");
let dotcordance = target.as_std_path().join(".cordance");
let status = Command::new("cmd")
.args([
"/C",
"mklink",
"/J",
dotcordance.to_str().expect("utf8 junction path"),
escape.path().to_str().expect("utf8 escape path"),
])
.status();
let Ok(status) = status else {
eprintln!("skipping: cmd.exe unavailable");
return;
};
if !status.success() {
eprintln!("skipping: mklink /J failed");
return;
}
let emitters: Vec<Box<dyn cordance_emit::TargetEmitter>> = vec![
Box::new(FakeEmitter {
name: "fake:clean",
outputs: vec![(Utf8PathBuf::from("AGENTS.md"), b"clean-bytes".to_vec())],
}),
Box::new(FakeEmitter {
name: "fake:hostile",
outputs: vec![(
Utf8PathBuf::from(".cordance/pack.json"),
b"would-write-through-junction".to_vec(),
)],
}),
];
let config = PackConfig {
target: target.clone(),
output_mode: OutputMode::Write,
selected_targets: PackTargets::default(),
doctrine_root: None,
llm_provider: None,
ollama_model: None,
quiet: true,
from_cortex_push: false,
cortex_receipt_requested_explicitly: false,
};
let pack = empty_pack(target.clone());
let err = dispatch_emitters(&emitters, &config, &pack)
.expect_err("planted junction ancestor must fail dispatch");
let msg = format!("{err:#}");
assert!(
msg.contains("symlink") || msg.contains("reparse-point"),
"error must indicate refusal class; got: {msg}"
);
assert!(
msg.contains(".cordance"),
"error must name the offending ancestor path; got: {msg}"
);
let agents_md = target.as_std_path().join("AGENTS.md");
assert!(
!agents_md.exists(),
"Phase-2 pre-check must fail BEFORE any emitter writes; \
AGENTS.md must not be on disk"
);
let escape_entries: Vec<_> = std::fs::read_dir(escape.path())
.expect("read escape")
.collect();
assert!(
escape_entries.is_empty(),
"no bytes may have leaked through the junction; escape tree \
must be empty, found: {escape_entries:?}"
);
}
#[cfg(windows)]
#[test]
fn pack_run_prechecks_metadata_junction_before_root_outputs_windows() {
use std::process::Command;
let (_target_dir, target) = temp_utf8_dir();
let (_doctrine_dir, doctrine_root) = temp_utf8_dir();
let escape = tempfile::tempdir().expect("escape tempdir");
let dotcordance = target.as_std_path().join(".cordance");
let status = Command::new("cmd")
.args([
"/C",
"mklink",
"/J",
dotcordance.to_str().expect("utf8 junction path"),
escape.path().to_str().expect("utf8 escape path"),
])
.status()
.expect("cmd.exe must be available for junction regression");
assert!(
status.success(),
"mklink /J must succeed for regression test"
);
let config = config_for_test(
&target,
OutputMode::Write,
PackTargets {
claude_code: true,
..Default::default()
},
&doctrine_root,
false,
false,
);
let err = run(&config).expect_err("planted .cordance junction must abort pack");
let msg = format!("{err:#}");
assert!(
msg.contains(".cordance"),
"error must name the refused metadata ancestor; got: {msg}"
);
assert!(
!target.join("AGENTS.md").exists(),
"metadata preflight must fail before root target output AGENTS.md lands"
);
let escape_entries: Vec<_> = std::fs::read_dir(escape.path())
.expect("read escape")
.collect();
assert!(
escape_entries.is_empty(),
"metadata bytes must not leak through the junction; found {escape_entries:?}"
);
}
#[cfg(unix)]
#[test]
fn pack_run_prechecks_metadata_symlink_before_root_outputs_unix() {
use std::os::unix::fs::symlink;
let (_target_dir, target) = temp_utf8_dir();
let (_doctrine_dir, doctrine_root) = temp_utf8_dir();
let escape = tempfile::tempdir().expect("escape tempdir");
let dotcordance = target.as_std_path().join(".cordance");
symlink(escape.path(), &dotcordance).expect("plant .cordance symlink");
let config = config_for_test(
&target,
OutputMode::Write,
PackTargets {
claude_code: true,
..Default::default()
},
&doctrine_root,
false,
false,
);
let err = run(&config).expect_err("planted .cordance symlink must abort pack");
let msg = format!("{err:#}");
assert!(
msg.contains(".cordance"),
"error must name the refused metadata ancestor; got: {msg}"
);
assert!(
!target.join("AGENTS.md").exists(),
"metadata preflight must fail before root target output AGENTS.md lands"
);
let escape_entries: Vec<_> = std::fs::read_dir(escape.path())
.expect("read escape")
.collect();
assert!(
escape_entries.is_empty(),
"metadata bytes must not leak through the symlink; found {escape_entries:?}"
);
}
}