use camino::Utf8PathBuf;
use cordance_core::pack::CordancePack;
use cordance_core::source::SourceClass;
use crate::{EmitError, TargetEmitter};
pub struct AgentsMdEmitter;
const DEFAULT_AXIOM_VERSION: &str = "v3.1.1-axiom";
const DEFAULT_AXIOM_SOURCE: &str = "../pai-axiom";
fn read_axiom_source_from_config(repo_root: &Utf8PathBuf) -> String {
#[derive(serde::Deserialize, Default)]
struct AxiomSection {
source: Option<String>,
}
#[derive(serde::Deserialize, Default)]
struct CordanceToml {
axiom: Option<AxiomSection>,
}
let config_path = repo_root.join("cordance.toml");
let Ok(content) = std::fs::read_to_string(&config_path) else {
return DEFAULT_AXIOM_SOURCE.to_string();
};
let Ok(parsed) = toml::from_str::<CordanceToml>(&content) else {
return DEFAULT_AXIOM_SOURCE.to_string();
};
parsed
.axiom
.and_then(|a| a.source)
.filter(|s| !s.is_empty())
.unwrap_or_else(|| DEFAULT_AXIOM_SOURCE.to_string())
}
pub(crate) fn resolve_axiom_info(pack: &CordancePack) -> (String, String) {
let repo_root = &pack.project.repo_root;
let axiom_source = read_axiom_source_from_config(repo_root);
let axiom_abs = if std::path::Path::new(&axiom_source).is_absolute() {
Utf8PathBuf::from(&axiom_source)
} else {
repo_root.join(&axiom_source)
};
let candidates = [
axiom_abs.join("PAI/Algorithm/LATEST"),
axiom_abs.join("axiom/Algorithm/LATEST"),
];
let version = candidates
.iter()
.find_map(|p| {
let content = std::fs::read_to_string(p).ok()?;
let v = content.trim().to_string();
if v.is_empty() {
None
} else {
Some(v)
}
})
.unwrap_or_else(|| DEFAULT_AXIOM_VERSION.to_string());
(version, axiom_source)
}
pub(crate) fn axiom_load_order_body(version: &str, axiom_source: &str) -> String {
let version = cordance_core::fence::sanitise_fenced_value(version);
let axiom_source = cordance_core::fence::sanitise_fenced_value(axiom_source);
let axiom_source_win = axiom_source.replace('/', "\\");
format!(
"## Axiom Load Order\n\
\n\
Axiom source (configure in `cordance.toml` → `[axiom].source`): `{axiom_source}`\n\
Algorithm version: `{version}`\n\
\n\
Load in order (prefer the path that resolves on your runtime):\n\
\n\
**POSIX / WSL:**\n\
- `{axiom_source}/PAI/Algorithm/{version}.md`\n\
- `{axiom_source}/PAI/CONTEXT_ROUTING.md`\n\
\n\
**Native Windows (replace `/` with `\\`):**\n\
- `{axiom_source_win}\\PAI\\Algorithm\\{version}.md`\n\
- `{axiom_source_win}\\PAI\\CONTEXT_ROUTING.md`\n\
\n\
Adjust `cordance.toml` → `[axiom].source` if axiom lives at a different path."
)
}
pub(crate) const fn hard_rules_body() -> &'static str {
"1. Deterministic-first. No LLM in the critical path.\n\
2. No runtime-root writes. Never write to `~/.claude`, `~/.codex`, `~/.Codex`.\n\
3. No cortex repo writes. Emit receipts; never modify cortex storage.\n\
4. Fenced regions are managed. Only edit content between fence markers.\n\
5. Source lock is truth. Run `cordance check` before claiming output is correct.\n\
6. Doctrine pins are immutable. Update doctrine pins only via `cordance pack`."
}
pub(crate) fn commands_body(pack: &CordancePack) -> String {
let has_justfile = pack.sources.iter().any(|s| {
s.path
.file_name()
.is_some_and(|n| n.eq_ignore_ascii_case("justfile"))
});
let has_cargo = pack.sources.iter().any(|s| {
s.class == SourceClass::ProjectSourceCode
&& s.path
.file_name()
.is_some_and(|n| n.eq_ignore_ascii_case("Cargo.toml"))
}) || pack.project.kind.contains("rust");
let has_package_json = pack.sources.iter().any(|s| {
s.path
.file_name()
.is_some_and(|n| n.eq_ignore_ascii_case("package.json"))
});
if has_justfile {
"```sh\njust check\njust build\njust test\n```".to_string()
} else if has_cargo {
"```sh\ncargo test --workspace\ncargo build --release\n```".to_string()
} else if has_package_json {
"```sh\nnpm test\nnpm run build\n```".to_string()
} else {
"No local task runner detected. Run `cordance advise` for a recommendation.".to_string()
}
}
pub(crate) const fn forbidden_surfaces_body() -> &'static str {
"Never import or commit these surfaces:\n\
- `.claude/{cache,sessions,worktrees,projects}/` — runtime exhaust\n\
- `.codex/{cache,sessions}/`, `.codex-logs/` — runtime exhaust\n\
- `*.env`, `*.env.local`, `*.env.production` — secrets\n\
- `node_modules/`, `target/`, `dist/`, `build/` — build artifacts\n\
- `*.sqlite`, `*.db` — binary state\n\
- `*.log` — log files\n\
- `*.pem`, `*.key` — credentials"
}
pub(crate) fn doctrine_pointers_body(pack: &CordancePack) -> String {
let commit_raw = pack
.doctrine_pins
.first()
.map_or("unpinned", |p| p.commit.as_str());
let commit_clean = cordance_core::fence::sanitise_fenced_value(commit_raw);
if pack.doctrine_pins.is_empty() {
format!(
"Engineering doctrine: https://github.com/0ryant/engineering-doctrine\n\
Key principles to load for this repo:\n\
- doctrine/principles/build.md\n\
- doctrine/principles/secure-development-lifecycle.md\n\
- doctrine/principles/modularity-and-ports-adapters.md\n\
- doctrine/principles/single-source-of-truth.md\n\
Doctrine commit: {commit_clean}"
)
} else {
let mut lines = vec![
"Engineering doctrine: https://github.com/0ryant/engineering-doctrine".to_string(),
"Pinned doctrine sources:".to_string(),
];
for pin in &pack.doctrine_pins {
let path_clean = cordance_core::fence::sanitise_fenced_value(pin.source_path.as_str());
let pin_commit_clean = cordance_core::fence::sanitise_fenced_value(&pin.commit);
lines.push(format!("- {path_clean} @ {pin_commit_clean}"));
}
lines.push(format!("Doctrine commit: {commit_clean}"));
lines.join("\n")
}
}
const TEMPLATE: &str = "\
# AGENTS.md
<!-- Generated by Cordance. Regions inside fences are managed; edits outside fences are preserved. -->
<!-- cordance:begin axiom-load-order -->
<!-- cordance:end axiom-load-order -->
## Project
<!-- cordance:begin hard-rules -->
<!-- cordance:end hard-rules -->
## Commands
<!-- cordance:begin commands -->
<!-- cordance:end commands -->
## Forbidden Surfaces
<!-- cordance:begin forbidden-surfaces -->
<!-- cordance:end forbidden-surfaces -->
## Doctrine
<!-- cordance:begin doctrine-pointers -->
<!-- cordance:end doctrine-pointers -->";
impl TargetEmitter for AgentsMdEmitter {
fn name(&self) -> &'static str {
"claude-code:agents-md"
}
fn render(&self, pack: &CordancePack) -> Result<Vec<(Utf8PathBuf, Vec<u8>)>, EmitError> {
let (version, axiom_source) = resolve_axiom_info(pack);
let load_order = axiom_load_order_body(&version, &axiom_source);
let hard_rules = hard_rules_body();
let commands = commands_body(pack);
let forbidden = forbidden_surfaces_body();
let doctrine = doctrine_pointers_body(pack);
let content = cordance_core::fence::replace_regions(
TEMPLATE,
&[
("axiom-load-order", &load_order),
("hard-rules", hard_rules),
("commands", &commands),
("forbidden-surfaces", forbidden),
("doctrine-pointers", &doctrine),
],
);
Ok(vec![("AGENTS.md".into(), content.into_bytes())])
}
}
#[cfg(test)]
mod tests {
use super::*;
fn write_cfg(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("cordance.toml"), content).expect("write");
(dir, target)
}
#[test]
fn axiom_source_reads_configured_value() {
let (_d, root) = write_cfg("[axiom]\nsource = \"/custom/path\"\n");
assert_eq!(read_axiom_source_from_config(&root), "/custom/path");
}
#[test]
fn axiom_source_handles_no_whitespace_around_equals() {
let (_d, root) = write_cfg("[axiom]\nsource=\"../other\"\n");
assert_eq!(read_axiom_source_from_config(&root), "../other");
}
#[test]
fn axiom_source_returns_default_when_file_missing() {
let dir = tempfile::tempdir().expect("tempdir");
let root = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).expect("tempdir is utf8");
assert_eq!(read_axiom_source_from_config(&root), DEFAULT_AXIOM_SOURCE);
}
#[test]
fn axiom_source_returns_default_when_section_absent() {
let (_d, root) = write_cfg("[doctrine]\nsource = \"x\"\n");
assert_eq!(read_axiom_source_from_config(&root), DEFAULT_AXIOM_SOURCE);
}
#[test]
fn axiom_source_returns_default_when_key_absent() {
let (_d, root) = write_cfg("[axiom]\nalgorithm_latest = \"auto\"\n");
assert_eq!(read_axiom_source_from_config(&root), DEFAULT_AXIOM_SOURCE);
}
#[test]
fn axiom_source_returns_default_when_value_empty() {
let (_d, root) = write_cfg("[axiom]\nsource = \"\"\n");
assert_eq!(read_axiom_source_from_config(&root), DEFAULT_AXIOM_SOURCE);
}
#[test]
fn axiom_source_returns_default_on_malformed_toml() {
let (_d, root) = write_cfg("not valid = = toml");
assert_eq!(read_axiom_source_from_config(&root), DEFAULT_AXIOM_SOURCE);
}
#[test]
fn axiom_source_ignores_unrelated_source_keys() {
let (_d, root) = write_cfg(
"[doctrine]\nsource = \"../engineering-doctrine\"\n\n[axiom]\nsource = \"../pai-axiom\"\n",
);
assert_eq!(read_axiom_source_from_config(&root), "../pai-axiom");
}
}