use clap::{Args, Subcommand};
use cortex_context::{
axiom_export_for_pack, ContextPackBuilder, ContextRefCandidate, ContextRefId, Sensitivity,
};
use cortex_core::{AuthorityClass, ClaimCeiling, RuntimeMode};
use cortex_retrieval::{
query_fts5, resolve_conflicts, AuthorityLevel, AuthorityProofHint, ConflictingMemoryInput,
ProofClosureHint,
};
use cortex_store::proof::verify_memory_proof_closure;
use cortex_store::repo::{ContradictionRepo, MemoryRecord, MemoryRepo, PrincipleRepo};
use cortex_store::Pool;
use std::collections::{BTreeMap, BTreeSet};
use crate::cmd::open_default_store;
use crate::exit::Exit;
use crate::output::{self, Envelope};
#[derive(Debug, Subcommand)]
pub enum ContextSub {
Build(BuildArgs),
}
#[derive(Debug, Args)]
pub struct BuildArgs {
#[arg(long)]
pub task: String,
#[arg(long = "max-tokens", default_value_t = 4096)]
pub max_tokens: usize,
#[arg(long = "axiom-constraints")]
pub axiom_constraints: bool,
#[arg(long = "tag", value_name = "TAG")]
pub tag: Vec<String>,
#[arg(long)]
pub fuzzy: bool,
#[arg(long)]
pub include_doctrine: bool,
}
pub fn run(sub: ContextSub) -> Exit {
match sub {
ContextSub::Build(args) => build(args),
}
}
fn build(args: BuildArgs) -> Exit {
if args.task.trim().is_empty() {
eprintln!("cortex context build: --task must not be empty");
return build_failure_envelope(Exit::Usage, "--task must not be empty");
}
if args.max_tokens == 0 {
eprintln!("cortex context build: --max-tokens must be greater than zero");
return build_failure_envelope(Exit::Usage, "--max-tokens must be greater than zero");
}
let axiom_constraints = args.axiom_constraints;
let include_doctrine = args.include_doctrine;
match build_pack(args) {
Ok(pack) => {
if let Err(err) = pack.require_default_use_allowed() {
eprintln!("cortex context build: {err}");
return build_failure_envelope(Exit::PreconditionUnmet, &err.to_string());
}
let mut payload = if axiom_constraints {
match serde_json::to_value(axiom_export_for_pack(&pack)) {
Ok(value) => value,
Err(err) => {
eprintln!("cortex context build: failed to serialize output: {err}");
return build_failure_envelope(
Exit::Internal,
&format!("failed to serialize output: {err}"),
);
}
}
} else {
match serde_json::to_value(&pack) {
Ok(value) => value,
Err(err) => {
eprintln!("cortex context build: failed to serialize output: {err}");
return build_failure_envelope(
Exit::Internal,
&format!("failed to serialize output: {err}"),
);
}
}
};
if include_doctrine {
if let Err(exit) = attach_doctrine_section(&mut payload) {
return build_failure_envelope(exit, "failed to read doctrine");
}
}
if output::json_enabled() {
let envelope = Envelope::new("cortex.context.build", Exit::Ok, payload);
return output::emit(&envelope, Exit::Ok);
}
match serde_json::to_string_pretty(&payload) {
Ok(serialized) => {
println!("{serialized}");
Exit::Ok
}
Err(err) => {
eprintln!("cortex context build: failed to serialize output: {err}");
Exit::Internal
}
}
}
Err(exit) => build_failure_envelope(exit, "context-pack build failed"),
}
}
fn attach_doctrine_section(payload: &mut serde_json::Value) -> Result<(), Exit> {
let pool = open_default_store("context build --include-doctrine")?;
let repo = PrincipleRepo::new(&pool);
let doctrine = repo.list_doctrine().map_err(|err| {
eprintln!("cortex context build: failed to read doctrine: {err}");
Exit::Internal
})?;
let entries: Vec<serde_json::Value> = doctrine
.iter()
.map(|d| {
serde_json::json!({
"doctrine_id": d.id.to_string(),
"source_principle": d.source_principle.to_string(),
"claim": d.rule,
"force": d.force,
"promotion_reason": d.promotion_reason,
"promoted_at": d.created_at.to_rfc3339(),
})
})
.collect();
if let Some(obj) = payload.as_object_mut() {
obj.insert("doctrine".to_string(), serde_json::Value::Array(entries));
}
Ok(())
}
fn build_failure_envelope(exit: Exit, detail: &str) -> Exit {
if !output::json_enabled() {
return exit;
}
let payload = serde_json::json!({
"status": "error",
"detail": detail,
});
let envelope = Envelope::new("cortex.context.build", exit, payload);
output::emit(&envelope, exit)
}
pub(crate) fn build_pack(args: BuildArgs) -> Result<cortex_context::ContextPack, Exit> {
let pool = open_default_store("context build")?;
let repo = MemoryRepo::new(&pool);
let mut active = if args.tag.is_empty() {
repo.list_by_status("active").map_err(|err| {
eprintln!("cortex context build: failed to read active memories: {err}");
Exit::Internal
})?
} else {
repo.list_by_status_with_tags("active", &args.tag)
.map_err(|err| {
eprintln!(
"cortex context build: failed to read tag-filtered active memories: {err}"
);
Exit::Internal
})?
};
if args.fuzzy {
let limit = std::cmp::max(active.len(), 16);
let fts_hits = query_fts5(&repo, &args.task, limit).map_err(|err| {
eprintln!("cortex context build: fuzzy retrieval failed: {err}");
Exit::Internal
})?;
let matched: BTreeSet<String> = fts_hits
.iter()
.map(|hit| hit.memory_id.to_string())
.collect();
active.retain(|memory| matched.contains(&memory.id.to_string()));
}
let mut builder = ContextPackBuilder::new(args.task, args.max_tokens);
for memory in &active {
let proof = verify_memory_proof_closure(&pool, &memory.id).map_err(|err| {
eprintln!(
"cortex context build: failed to verify memory {} proof closure: {err}",
memory.id
);
Exit::PreconditionUnmet
})?;
if let Err(err) = proof.require_current_use_allowed() {
eprintln!(
"cortex context build: memory {} excluded from default context use: {err}",
memory.id
);
return Err(Exit::PreconditionUnmet);
}
builder = builder.select_ref(
ContextRefCandidate::new(
ContextRefId::Memory {
memory_id: memory.id,
},
memory.claim.clone(),
)
.with_claim_metadata(
RuntimeMode::LocalUnsigned,
AuthorityClass::Derived,
proof.state().into(),
ClaimCeiling::LocalUnsigned,
)
.with_sensitivity(Sensitivity::Internal),
);
}
if let Err(err) = gate_open_contradictions_for_default_context(&pool, &active) {
eprintln!("cortex context build: {err}");
return Err(Exit::PreconditionUnmet);
}
builder.build().map_err(|err| {
eprintln!("cortex context build: {err}");
Exit::PreconditionUnmet
})
}
fn gate_open_contradictions_for_default_context(
pool: &Pool,
memories: &[MemoryRecord],
) -> Result<(), String> {
let active_by_id = memories
.iter()
.map(|memory| (memory.id.to_string(), memory))
.collect::<BTreeMap<_, _>>();
let contradictions = ContradictionRepo::new(pool)
.list_open()
.map_err(|err| format!("failed to read open contradictions: {err}"))?;
let mut affected_ids = BTreeSet::new();
let mut conflict_edges = BTreeMap::<String, BTreeSet<String>>::new();
for contradiction in contradictions {
let left_active = active_by_id.contains_key(&contradiction.left_ref);
let right_active = active_by_id.contains_key(&contradiction.right_ref);
if !left_active && !right_active {
continue;
}
if !(left_active && right_active) {
return Err(format!(
"open contradiction {} references unavailable memory and cannot be resolved for default context-pack use",
contradiction.id
));
}
affected_ids.insert(contradiction.left_ref.clone());
affected_ids.insert(contradiction.right_ref.clone());
conflict_edges
.entry(contradiction.left_ref.clone())
.or_default()
.insert(contradiction.right_ref.clone());
conflict_edges
.entry(contradiction.right_ref)
.or_default()
.insert(contradiction.left_ref);
}
if affected_ids.is_empty() {
return Ok(());
}
let inputs = affected_ids
.iter()
.filter_map(|id| active_by_id.get(id.as_str()).copied())
.map(|memory| {
ConflictingMemoryInput::new(
memory.id.to_string(),
Some(memory.id.to_string()),
memory.claim.clone(),
AuthorityProofHint {
authority: authority_level(&memory.authority),
proof: ProofClosureHint::FullChainVerified,
},
)
.with_conflicts(
conflict_edges
.get(&memory.id.to_string())
.map(|ids| ids.iter().cloned().collect())
.unwrap_or_default(),
)
})
.collect::<Vec<_>>();
let output = resolve_conflicts(&inputs, &[]);
output
.require_default_use_allowed()
.map_err(|err| format!("open contradiction blocks default context-pack use: {err}"))
}
fn authority_level(authority: &str) -> AuthorityLevel {
match authority {
"user" | "operator" => AuthorityLevel::High,
"tool" | "system" => AuthorityLevel::Medium,
_ => AuthorityLevel::Low,
}
}