mod toml;
pub mod standards;
pub mod resolution;
pub mod projection;
use crate::gate::{ArtefactRef, Gate, GateKind, GateOutcome};
use crate::types::{MissionConfig, Role};
use std::collections::HashSet;
use std::path::{Component, Path, PathBuf};
pub const PACK_MANIFEST: &str = "pack.toml";
pub const SCHEMA_BASE: u32 = 2;
pub const SCHEMA_CONTRACT: u32 = 3;
pub const SCHEMA_STANDARDS: u32 = 4;
pub const RESERVED_GATE_NAMES: &[&str] = &[
crate::contract_gates::VACUOUS_FILTER,
crate::contract_gates::WRONG_POLARITY,
crate::contract_gates::PASSES_ON_BASE,
crate::contract_gates::ENV_SENSITIVE,
"merge-gate-suite",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pack {
pub name: String,
pub schema: u32,
pub dir: PathBuf,
pub gates: Vec<PackGateDecl>,
pub prompts: Vec<PackPrompt>,
pub checklists: Vec<PackChecklist>,
pub artefact_stores: Vec<PackArtefactStore>,
pub standards: Option<standards::StandardsManifest>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackGateDecl {
pub name: String,
pub command: String,
pub when_paths: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackPrompt {
pub name: String,
pub role: Role,
pub text: String,
pub source: PromptSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PromptSource {
Inline,
File(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackChecklist {
pub name: String,
pub items: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackArtefactStore {
pub name: String,
pub kind: String,
}
impl Pack {
pub fn load(dir: &Path) -> Result<Option<Pack>, String> {
Self::load_with_trust(dir, standards::StandardsTrust::External)
}
pub fn load_with_trust(
dir: &Path,
trust: standards::StandardsTrust,
) -> Result<Option<Pack>, String> {
let manifest_path = dir.join(PACK_MANIFEST);
let source = match std::fs::read_to_string(&manifest_path) {
Ok(source) => source,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(format!("cannot read {}: {e}", manifest_path.display())),
};
Self::parse_with_trust(dir, &source, trust).map(Some)
}
pub fn parse(dir: &Path, source: &str) -> Result<Pack, String> {
Self::parse_with_trust(dir, source, standards::StandardsTrust::External)
}
pub fn parse_with_trust(
dir: &Path,
source: &str,
trust: standards::StandardsTrust,
) -> Result<Pack, String> {
let doc = toml::parse(source).map_err(|e| format!("{PACK_MANIFEST}: {e}"))?;
Self::from_document(dir, &doc, trust)
}
fn from_document(
dir: &Path,
doc: &toml::Document,
trust: standards::StandardsTrust,
) -> Result<Pack, String> {
for section in &doc.sections {
let name = match section {
toml::Section::Single(t) => t.name.as_str(),
toml::Section::Array { name, .. } => name.as_str(),
};
if ![
"pack",
"gate",
"prompt",
"checklist",
"artefact_store",
"standards",
]
.contains(&name)
{
return Err(format!(
"{PACK_MANIFEST}: unknown section `{name}` (declared sections: [pack], \
[[gate]], [[prompt]], [[checklist]], [[artefact_store]], [standards])"
));
}
}
let (name, schema) = manifest_header(doc)?;
let mut gates = Vec::new();
for (idx, item) in doc.array("gate").iter().enumerate() {
gates.push(load_gate(item, idx)?);
}
reject_duplicate_names("gate", gates.iter().map(|g| g.name.as_str()))?;
let mut prompts = Vec::new();
for (idx, item) in doc.array("prompt").iter().enumerate() {
prompts.push(load_prompt(item, idx, dir)?);
}
reject_duplicate_names("prompt", prompts.iter().map(|p| p.name.as_str()))?;
let mut checklists = Vec::new();
for (idx, item) in doc.array("checklist").iter().enumerate() {
checklists.push(load_checklist(item, idx)?);
}
reject_duplicate_names("checklist", checklists.iter().map(|c| c.name.as_str()))?;
let mut artefact_stores = Vec::new();
for (idx, item) in doc.array("artefact_store").iter().enumerate() {
artefact_stores.push(load_artefact_store(item, idx)?);
}
reject_duplicate_names(
"artefact_store",
artefact_stores.iter().map(|s| s.name.as_str()),
)?;
let standards = match standards_root_of(doc, schema)? {
Some(root) => Some(standards::load_from_pack_dir(dir, &root, &gates, trust)?),
None => None,
};
Ok(Pack {
name,
schema,
dir: dir.to_path_buf(),
gates,
prompts,
checklists,
artefact_stores,
standards,
})
}
pub fn gates_for_paths(&self, changed_paths: &[String]) -> Vec<&PackGateDecl> {
self.gates
.iter()
.filter(|g| crate::merge_gate::when_paths_match(&g.when_paths, changed_paths))
.collect()
}
pub fn prompt_section(&self, role: Role) -> String {
let mut section = String::new();
for prompt in self.prompts.iter().filter(|p| p.role == role) {
section.push_str(&format!(
"\n\n---\nPack guidance (pack `{}`, prompt `{}`):\n{}\n",
self.name,
prompt.name,
prompt.text.trim_end()
));
}
section
}
pub fn describe(&self) -> String {
let gate_names = self
.gates
.iter()
.map(|g| g.name.as_str())
.collect::<Vec<_>>()
.join(", ");
let prompt_names = self
.prompts
.iter()
.map(|p| format!("{}→{}", p.name, role_target_name(p.role)))
.collect::<Vec<_>>()
.join(", ");
let standards = match &self.standards {
Some(m) => format!(
", standards: {} RFC(s)/{} rule(s) digest sha256:{}",
m.rfcs.len(),
m.rules.len(),
m.digest
),
None => String::new(),
};
format!(
"pack `{}` (schema {}) at {}: {} gate(s) [{}], {} prompt(s) [{}], \
{} checklist(s), {} artefact store(s) (checklists/stores are \
declaration-only: validated at load, never executed){standards}",
self.name,
self.schema,
self.dir.display(),
self.gates.len(),
gate_names,
self.prompts.len(),
prompt_names,
self.checklists.len(),
self.artefact_stores.len(),
)
}
}
pub fn load_for_config(cfg: &MissionConfig, repo_root: &Path) -> Result<Option<Pack>, String> {
let Some(configured) = cfg.pack_dir.as_deref() else {
return Ok(None);
};
let raw = Path::new(configured);
let (dir, trust) = if raw.is_absolute() {
(raw.to_path_buf(), standards::StandardsTrust::External)
} else {
validate_pack_relative_path(configured, "mission config", "packDir")?;
let dir = repo_root.join(raw);
let trust = standards::trust_for_dir(repo_root, &dir);
(dir, trust)
};
if !dir.is_dir() {
return Err(format!(
"packDir `{configured}` resolves to {}, which is not a directory",
dir.display()
));
}
let Some(pack) = Pack::load_with_trust(&dir, trust)? else {
return Err(format!(
"packDir `{configured}` resolves to {}, which has no {PACK_MANIFEST} — \
it is not a pack",
dir.display()
));
};
Ok(Some(pack))
}
pub fn render_lint(pack: &Pack) -> String {
let mut out = format!(
"pack `{}` (schema {}) at {} — valid\n",
pack.name,
pack.schema,
pack.dir.display()
);
out.push_str("gates (deterministic; final-gate, after the engine floor, advisory):\n");
if pack.gates.is_empty() {
out.push_str(" (none)\n");
}
for gate in &pack.gates {
let scoping = if gate.when_paths.is_empty() {
"unconditional".to_string()
} else {
format!("whenPaths: {}", gate.when_paths.join(", "))
};
out.push_str(&format!(
" - {}: `{}` ({scoping})\n",
gate.name, gate.command
));
}
out.push_str("prompts (appended to the target role's prompt):\n");
if pack.prompts.is_empty() {
out.push_str(" (none)\n");
}
for prompt in &pack.prompts {
let source = match &prompt.source {
PromptSource::Inline => "inline text".to_string(),
PromptSource::File(rel) => format!("file {rel}"),
};
out.push_str(&format!(
" - {} → {} ({source})\n",
prompt.name,
role_target_name(prompt.role)
));
}
out.push_str("checklists (declaration-only: validated at load, never executed):\n");
if pack.checklists.is_empty() {
out.push_str(" (none)\n");
}
for checklist in &pack.checklists {
out.push_str(&format!(
" - {} ({} item(s))\n",
checklist.name,
checklist.items.len()
));
}
out.push_str("artefact stores (declaration-only: validated at load, never invoked):\n");
if pack.artefact_stores.is_empty() {
out.push_str(" (none)\n");
}
for store in &pack.artefact_stores {
out.push_str(&format!(" - {} (kind `{}`)\n", store.name, store.kind));
}
if let Some(manifest) = &pack.standards {
out.push_str(&standards::render_registration(manifest));
}
out
}
pub fn role_target_name(role: Role) -> &'static str {
match role {
Role::Worker => "worker",
Role::ValidatorScrutiny => "validator-scrutiny",
Role::ValidatorFunctional => "validator-functional",
Role::Orchestrator => "orchestrator",
}
}
pub struct PackGate {
name: String,
outcome: GateOutcome,
}
impl PackGate {
pub fn from_run(name: &str, command: &str, ok: bool, output: String) -> Self {
let artefact = ArtefactRef::new(command.to_string());
let outcome = if ok {
GateOutcome::pass(artefact)
} else {
GateOutcome::fail(artefact.with_detail(output))
};
Self {
name: name.to_string(),
outcome,
}
}
pub fn with_rule_ids(mut self, rule_ids: Vec<String>) -> Self {
self.outcome = self.outcome.with_rule_ids(rule_ids);
self
}
}
impl Gate for PackGate {
fn name(&self) -> &str {
&self.name
}
fn kind(&self) -> GateKind {
GateKind::Deterministic
}
fn evaluate(&self) -> GateOutcome {
self.outcome.clone()
}
}
fn manifest_header(doc: &toml::Document) -> Result<(String, u32), String> {
let header = doc
.single("pack")
.ok_or_else(|| format!("{PACK_MANIFEST}: missing required table `[pack]`"))?;
check_unknown(header, "[pack]", &["name", "schema"])?;
let name = required_string(header, "[pack]", "name")?;
let schema = match header.get("schema") {
Some(toml::Value::Integer(n)) => {
let n = *n;
if n == i64::from(SCHEMA_BASE)
|| n == i64::from(SCHEMA_CONTRACT)
|| n == i64::from(SCHEMA_STANDARDS)
{
n as u32
} else {
return Err(format!(
"[pack] field `schema` is {n}: supported versions are {SCHEMA_BASE} \
(base manifest), {SCHEMA_CONTRACT} (contract), and {SCHEMA_STANDARDS} \
(standards)"
));
}
}
Some(v) => {
return Err(format!(
"[pack] field `schema` must be an integer, got {}",
v.type_name()
))
}
None => return Err("[pack] is missing required field `schema`".to_string()),
};
Ok((name, schema))
}
fn standards_root_of(doc: &toml::Document, schema: u32) -> Result<Option<String>, String> {
let Some(table) = doc.single("standards") else {
return Ok(None);
};
if schema != SCHEMA_STANDARDS {
return Err(format!(
"[standards] requires [pack] field `schema` = {SCHEMA_STANDARDS} (this pack \
declares schema {schema}) — the standards root is additive at schema \
{SCHEMA_STANDARDS} only"
));
}
check_unknown(table, "[standards]", &["root"])?;
let raw = required_string(table, "[standards]", "root")?;
validate_pack_relative_path(&raw, "[standards]", "root")?;
let normalized = crate::merge_gate::normalize_relative_path(&raw, false);
if normalized.is_empty() || normalized == "." {
return Err("[standards] field `root` must name a pack-relative directory".to_string());
}
Ok(Some(normalized))
}
fn entry_label(section: &str, index: usize, table: &toml::Table) -> String {
match table.get("name") {
Some(toml::Value::String(name)) => {
format!("[[{section}]] entry {} (name `{name}`)", index + 1)
}
_ => format!("[[{section}]] entry {}", index + 1),
}
}
fn check_unknown(table: &toml::Table, section: &str, known: &[&str]) -> Result<(), String> {
let unknown = table.unknown_keys(known);
if let Some(field) = unknown.first() {
return Err(format!(
"{section} has unknown field `{field}` (declared fields: {})",
known.join(", ")
));
}
Ok(())
}
fn required_string(table: &toml::Table, section: &str, key: &str) -> Result<String, String> {
match table.get(key) {
Some(toml::Value::String(s)) if !s.trim().is_empty() => Ok(s.clone()),
Some(toml::Value::String(_)) => Err(format!(
"{section} field `{key}` must be a non-empty string"
)),
Some(v) => Err(format!(
"{section} field `{key}` must be a string, got {}",
v.type_name()
)),
None => Err(format!("{section} is missing required field `{key}`")),
}
}
fn optional_string(
table: &toml::Table,
section: &str,
key: &str,
) -> Result<Option<String>, String> {
match table.get(key) {
Some(toml::Value::String(s)) => Ok(Some(s.clone())),
Some(v) => Err(format!(
"{section} field `{key}` must be a string, got {}",
v.type_name()
)),
None => Ok(None),
}
}
fn optional_string_array(
table: &toml::Table,
section: &str,
key: &str,
) -> Result<Vec<String>, String> {
match table.get(key) {
Some(toml::Value::Array(items)) => {
let mut out = Vec::with_capacity(items.len());
for (idx, item) in items.iter().enumerate() {
match item {
toml::Value::String(s) if !s.trim().is_empty() => out.push(s.clone()),
toml::Value::String(_) => {
return Err(format!(
"{section} field `{key}` element {} must be a non-empty string",
idx + 1
))
}
v => {
return Err(format!(
"{section} field `{key}` element {} must be a string, got {}",
idx + 1,
v.type_name()
))
}
}
}
Ok(out)
}
Some(v) => Err(format!(
"{section} field `{key}` must be an array of strings, got {}",
v.type_name()
)),
None => Ok(Vec::new()),
}
}
fn load_gate(table: &toml::Table, index: usize) -> Result<PackGateDecl, String> {
let section = entry_label("gate", index, table);
check_unknown(table, §ion, &["name", "kind", "command", "whenPaths"])?;
let name = required_string(table, §ion, "name")?;
if let Some(kind) = optional_string(table, §ion, "kind")? {
if kind != "deterministic" {
return Err(format!(
"{section} field `kind` is `{kind}`: packs may register only deterministic \
gates in this slice — model-judged gates are declared by the engine, \
never by a pack"
));
}
}
if RESERVED_GATE_NAMES.contains(&name.as_str()) {
return Err(format!(
"{section} field `name` is `{name}`: reserved for an engine floor gate — \
a pack can add gates after the floor, never impersonate it"
));
}
let command = required_string(table, §ion, "command")?;
if command.contains(['\n', '\r', '\0']) {
return Err(format!(
"{section} field `command` must be a single non-NUL line"
));
}
let mut when_paths = Vec::new();
for raw in optional_string_array(table, §ion, "whenPaths")? {
validate_pack_relative_path(&raw, §ion, "whenPaths")?;
let normalized = crate::merge_gate::normalize_relative_path(&raw, false);
if normalized.is_empty() || normalized == "." {
return Err(format!(
"{section} field `whenPaths` entries must name a repo path — \
omit whenPaths to run unconditionally"
));
}
when_paths.push(normalized);
}
Ok(PackGateDecl {
name,
command,
when_paths,
})
}
fn load_prompt(table: &toml::Table, index: usize, pack_dir: &Path) -> Result<PackPrompt, String> {
let section = entry_label("prompt", index, table);
check_unknown(table, §ion, &["name", "role", "text", "textFile"])?;
let name = required_string(table, §ion, "name")?;
let role_raw = required_string(table, §ion, "role")?;
let role = match role_raw.as_str() {
"worker" => Role::Worker,
"validator-scrutiny" => Role::ValidatorScrutiny,
"validator-functional" => Role::ValidatorFunctional,
other => {
return Err(format!(
"{section} field `role` is `{other}`: supported targets are `worker`, \
`validator-scrutiny`, `validator-functional` (the session roles whose \
prompts runner.rs builds)"
))
}
};
let inline = optional_string(table, §ion, "text")?;
let file = optional_string(table, §ion, "textFile")?;
let (text, source) = match (inline, file) {
(Some(_), Some(_)) => {
return Err(format!(
"{section} declares both `text` and `textFile` — exactly one is required"
))
}
(None, None) => {
return Err(format!(
"{section} is missing required field `text` (or `textFile`)"
))
}
(Some(text), None) => (text, PromptSource::Inline),
(None, Some(rel)) => {
validate_pack_relative_path(&rel, §ion, "textFile")?;
let normalized = crate::merge_gate::normalize_relative_path(&rel, false);
let text = read_pack_text_file_nofollow(pack_dir, &normalized, §ion)?;
(text, PromptSource::File(normalized))
}
};
if text.trim().is_empty() {
return Err(format!(
"{section} field `text` resolves to empty prompt text"
));
}
Ok(PackPrompt {
name,
role,
text,
source,
})
}
fn load_checklist(table: &toml::Table, index: usize) -> Result<PackChecklist, String> {
let section = entry_label("checklist", index, table);
check_unknown(table, §ion, &["name", "items"])?;
let name = required_string(table, §ion, "name")?;
if table.get("items").is_none() {
return Err(format!("{section} is missing required field `items`"));
}
let items = optional_string_array(table, §ion, "items")?;
if items.is_empty() {
return Err(format!(
"{section} field `items` must list at least one item"
));
}
Ok(PackChecklist { name, items })
}
fn load_artefact_store(table: &toml::Table, index: usize) -> Result<PackArtefactStore, String> {
let section = entry_label("artefact_store", index, table);
check_unknown(table, §ion, &["name", "kind"])?;
let name = required_string(table, §ion, "name")?;
let kind = required_string(table, §ion, "kind")?;
Ok(PackArtefactStore { name, kind })
}
fn reject_duplicate_names<'a>(
section: &str,
names: impl Iterator<Item = &'a str>,
) -> Result<(), String> {
let mut seen = HashSet::new();
for name in names {
if !seen.insert(name) {
return Err(format!("duplicate [[{section}]] name `{name}`"));
}
}
Ok(())
}
fn validate_pack_relative_path(raw: &str, section: &str, field: &str) -> Result<(), String> {
let path = Path::new(raw);
if raw.trim().is_empty()
|| path.is_absolute()
|| path
.components()
.any(|part| !matches!(part, Component::CurDir | Component::Normal(_)))
{
return Err(format!(
"{section} field `{field}` must be a pack-relative path without parent \
components: {raw:?}"
));
}
Ok(())
}
fn read_pack_text_file_nofollow(
pack_dir: &Path,
rel: &str,
section: &str,
) -> Result<String, String> {
use cap_fs_ext::{DirExt as _, FollowSymlinks, OpenOptionsFollowExt as _};
use std::io::Read as _;
let display = pack_dir.join(rel);
let field_error = |message: String| format!("{section} field `textFile` = {rel:?} {message}");
let no_follow_refusal = |what: &str| {
field_error(format!(
"resolves through {what} ({}) — pack prompt files load no-follow so a pack \
cannot read outside its own directory",
display.display()
))
};
let mut dir = cap_std::fs::Dir::open_ambient_dir(pack_dir, cap_std::ambient_authority())
.map_err(|e| field_error(format!("cannot open pack dir {}: {e}", pack_dir.display())))?;
let mut names = rel.split('/').peekable();
while let Some(name) = names.next() {
if names.peek().is_some() {
dir = dir
.open_dir_nofollow(name)
.map_err(|_| no_follow_refusal("a symlinked or non-directory component"))?;
} else {
let mut options = cap_std::fs::OpenOptions::new();
options.read(true).follow(FollowSymlinks::No);
let mut file = dir.open_with(name, &options).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
field_error(format!("cannot be read at {}: {e}", display.display()))
} else {
no_follow_refusal("a symlink or other non-regular file")
}
})?;
let mut text = String::new();
file.read_to_string(&mut text).map_err(|e| {
field_error(format!("cannot be read at {}: {e}", display.display()))
})?;
return Ok(text);
}
}
Err(field_error("resolves to no file".to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gate::GatePipeline;
fn pack_dir_with(manifest: &str, files: &[(&str, &str)]) -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("pack");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(PACK_MANIFEST), manifest).unwrap();
for (rel, body) in files {
let path = dir.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, body).unwrap();
}
(tmp, dir)
}
const FULL_MANIFEST: &str = r#"
[pack]
name = "zz-synthetic-pack"
schema = 3
[[gate]]
name = "zz-gate-one"
command = "cd ."
[[gate]]
name = "zz-gate-two"
command = "cd ."
whenPaths = ["src/"]
[[prompt]]
name = "zz-prompt-worker"
role = "worker"
text = "zz inline worker guidance"
[[prompt]]
name = "zz-prompt-scrutiny"
role = "validator-scrutiny"
textFile = "prompts/scrutiny.md"
[[checklist]]
name = "zz-checklist"
items = ["first", "second"]
[[artefact_store]]
name = "zz-store"
kind = "local-dir"
"#;
#[test]
fn pack_contract_full_pack_loads_all_sections() {
let (_tmp, dir) =
pack_dir_with(FULL_MANIFEST, &[("prompts/scrutiny.md", "zz file text\n")]);
let pack = Pack::load(&dir).expect("load").expect("a pack");
assert_eq!(pack.name, "zz-synthetic-pack");
assert_eq!(pack.schema, SCHEMA_CONTRACT);
assert_eq!(pack.gates.len(), 2);
assert_eq!(pack.gates[1].when_paths, vec!["src".to_string()]);
assert_eq!(pack.prompts.len(), 2);
assert_eq!(pack.prompts[1].text, "zz file text\n");
assert_eq!(
pack.prompts[1].source,
PromptSource::File("prompts/scrutiny.md".to_string())
);
assert_eq!(pack.checklists[0].items.len(), 2);
assert_eq!(pack.artefact_stores[0].kind, "local-dir");
}
#[test]
fn pack_contract_schema_two_base_manifest_registers_nothing() {
let (_tmp, dir) = pack_dir_with("[pack]\nname = \"kranz\"\nschema = 2\n", &[]);
let pack = Pack::load(&dir).expect("load").expect("a pack");
assert_eq!(pack.schema, SCHEMA_BASE);
assert!(pack.gates.is_empty());
assert!(pack.prompts.is_empty());
assert!(pack.checklists.is_empty());
assert!(pack.artefact_stores.is_empty());
}
#[test]
fn pack_contract_directory_without_manifest_is_not_a_pack() {
let tmp = tempfile::tempdir().unwrap();
assert_eq!(Pack::load(tmp.path()).expect("load"), None);
}
#[test]
fn pack_contract_unknown_field_fails_closed() {
let (_tmp, dir) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[gate]]\nname = \"g\"\ncommand = \"true\"\nbogus = 1\n",
&[],
);
let err = Pack::load(&dir).expect_err("must fail");
assert!(err.contains("unknown field `bogus`"), "{err}");
assert!(err.contains("[[gate]]"), "{err}");
}
#[test]
fn pack_contract_unknown_section_fails_closed() {
let (_tmp, dir) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[gates]]\nname = \"g\"\ncommand = \"true\"\n",
&[],
);
let err = Pack::load(&dir).expect_err("must fail");
assert!(err.contains("unknown section `gates`"), "{err}");
}
#[test]
fn pack_contract_missing_required_key_fails_closed() {
let (_tmp, dir) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[gate]]\nname = \"g\"\n",
&[],
);
let err = Pack::load(&dir).expect_err("must fail");
assert!(err.contains("missing required field `command`"), "{err}");
let (_tmp2, dir2) = pack_dir_with("[pack]\nname = \"x\"\n", &[]);
let err = Pack::load(&dir2).expect_err("must fail");
assert!(err.contains("missing required field `schema`"), "{err}");
}
#[test]
fn pack_contract_wrong_type_fails_closed() {
let (_tmp, dir) = pack_dir_with("[pack]\nname = \"x\"\nschema = \"3\"\n", &[]);
let err = Pack::load(&dir).expect_err("must fail");
assert!(err.contains("field `schema` must be an integer"), "{err}");
let (_tmp2, dir2) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[gate]]\nname = \"g\"\ncommand = \"true\"\nwhenPaths = \"src\"\n",
&[],
);
let err = Pack::load(&dir2).expect_err("must fail");
assert!(
err.contains("field `whenPaths` must be an array of strings"),
"{err}"
);
}
#[test]
fn pack_contract_duplicate_name_fails_closed() {
let (_tmp, dir) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[gate]]\nname = \"g\"\ncommand = \"true\"\n\n[[gate]]\nname = \"g\"\ncommand = \"false\"\n",
&[],
);
let err = Pack::load(&dir).expect_err("must fail");
assert!(err.contains("duplicate [[gate]] name `g`"), "{err}");
}
#[test]
fn pack_contract_model_judged_gate_kind_refused() {
let (_tmp, dir) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[gate]]\nname = \"g\"\ncommand = \"true\"\nkind = \"model-judged\"\n",
&[],
);
let err = Pack::load(&dir).expect_err("must fail");
assert!(err.contains("field `kind` is `model-judged`"), "{err}");
assert!(err.contains("deterministic"), "{err}");
}
#[test]
fn pack_contract_engine_floor_gate_names_are_reserved() {
for reserved in RESERVED_GATE_NAMES {
let manifest = format!(
"[pack]\nname = \"x\"\nschema = 3\n\n[[gate]]\nname = \"{reserved}\"\ncommand = \"true\"\n"
);
let (_tmp, dir) = pack_dir_with(&manifest, &[]);
let err = Pack::load(&dir).expect_err("must fail");
assert!(err.contains("reserved for an engine floor gate"), "{err}");
}
}
#[test]
fn pack_contract_empty_gate_command_fails_closed() {
let (_tmp, dir) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[gate]]\nname = \"g\"\ncommand = \" \"\n",
&[],
);
let err = Pack::load(&dir).expect_err("must fail");
assert!(
err.contains("field `command` must be a non-empty string"),
"{err}"
);
}
#[test]
fn pack_contract_unsupported_schema_fails_closed() {
let (_tmp, dir) = pack_dir_with("[pack]\nname = \"x\"\nschema = 5\n", &[]);
let err = Pack::load(&dir).expect_err("must fail");
assert!(err.contains("field `schema` is 5"), "{err}");
}
#[test]
fn pack_contract_prompt_text_and_textfile_are_exclusive() {
let (_tmp, dir) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[prompt]]\nname = \"p\"\nrole = \"worker\"\ntext = \"t\"\ntextFile = \"p.md\"\n",
&[],
);
let err = Pack::load(&dir).expect_err("must fail");
assert!(err.contains("both `text` and `textFile`"), "{err}");
let (_tmp2, dir2) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[prompt]]\nname = \"p\"\nrole = \"worker\"\n",
&[],
);
let err = Pack::load(&dir2).expect_err("must fail");
assert!(err.contains("missing required field `text`"), "{err}");
}
#[test]
fn pack_contract_prompt_role_must_target_a_session_role() {
let (_tmp, dir) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[prompt]]\nname = \"p\"\nrole = \"orchestrator\"\ntext = \"t\"\n",
&[],
);
let err = Pack::load(&dir).expect_err("must fail");
assert!(err.contains("field `role` is `orchestrator`"), "{err}");
}
#[test]
fn pack_contract_prompt_textfile_must_stay_inside_the_pack() {
let (_tmp, dir) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[prompt]]\nname = \"p\"\nrole = \"worker\"\ntextFile = \"../escape.md\"\n",
&[],
);
let err = Pack::load(&dir).expect_err("must fail");
assert!(
err.contains("field `textFile` must be a pack-relative path"),
"{err}"
);
let (_tmp2, dir2) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[prompt]]\nname = \"p\"\nrole = \"worker\"\ntextFile = \"missing.md\"\n",
&[],
);
let err = Pack::load(&dir2).expect_err("must fail");
assert!(
err.contains("field `textFile` = \"missing.md\" cannot be read"),
"{err}"
);
}
#[test]
fn pack_contract_checklist_requires_items() {
let (_tmp, dir) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[checklist]]\nname = \"c\"\n",
&[],
);
let err = Pack::load(&dir).expect_err("must fail");
assert!(err.contains("missing required field `items`"), "{err}");
}
#[test]
fn pack_contract_prompt_section_targets_only_the_named_role() {
let (_tmp, dir) =
pack_dir_with(FULL_MANIFEST, &[("prompts/scrutiny.md", "zz file text\n")]);
let pack = Pack::load(&dir).unwrap().unwrap();
let worker = pack.prompt_section(Role::Worker);
assert!(worker.contains("zz-prompt-worker"), "{worker}");
assert!(worker.contains("zz inline worker guidance"), "{worker}");
assert!(!worker.contains("zz-prompt-scrutiny"), "{worker}");
let scrutiny = pack.prompt_section(Role::ValidatorScrutiny);
assert!(scrutiny.contains("zz file text"), "{scrutiny}");
assert!(!scrutiny.contains("zz-prompt-worker"), "{scrutiny}");
assert_eq!(pack.prompt_section(Role::ValidatorFunctional), "");
}
#[test]
fn pack_contract_when_paths_scope_gates_like_the_merge_suite() {
let (_tmp, dir) =
pack_dir_with(FULL_MANIFEST, &[("prompts/scrutiny.md", "zz file text\n")]);
let pack = Pack::load(&dir).unwrap().unwrap();
let changed = vec!["crates/engine/src/lib.rs".to_string()];
let applicable: Vec<&str> = pack
.gates_for_paths(&changed)
.iter()
.map(|g| g.name.as_str())
.collect();
assert_eq!(applicable, vec!["zz-gate-one"], "scoped gate skipped");
let changed = vec!["src/widget.ts".to_string()];
let applicable: Vec<&str> = pack
.gates_for_paths(&changed)
.iter()
.map(|g| g.name.as_str())
.collect();
assert_eq!(applicable, vec!["zz-gate-one", "zz-gate-two"]);
}
#[test]
fn pack_contract_gates_never_precede_or_displace_engine_floor_gates() {
use crate::types::{Assertion, AssertionCheck};
let contract = vec![Assertion {
id: "a1".to_string(),
statement: "s".to_string(),
check: AssertionCheck::Command,
command: Some("cargo test --workspace zz_pack_contract_floor 2>&1 | grep -qE 'test result: ok\\. [1-9]'".to_string()),
negative_control: None,
pty_script: None,
}];
let tree = tempfile::tempdir().unwrap();
let mut pipeline = GatePipeline::new();
crate::contract_gates::register_contract_gates(&mut pipeline, &contract, None, tree.path());
let floor_len = pipeline.len();
assert!(floor_len > 0, "floor gates registered");
pipeline.register(Box::new(PackGate::from_run(
"zz-pack-gate",
"cd .",
true,
String::new(),
)));
let reports = pipeline.evaluate();
let names: Vec<&str> = reports.iter().map(|r| r.name.as_str()).collect();
assert_eq!(
names.last(),
Some(&"zz-pack-gate"),
"the pack gate evaluates LAST: {names:?}"
);
let floor_names = &names[..floor_len];
assert!(floor_names.contains(&crate::contract_gates::VACUOUS_FILTER));
assert!(floor_names.contains(&crate::contract_gates::ENV_SENSITIVE));
assert!(
!floor_names.contains(&"zz-pack-gate"),
"no pack gate inside the floor section"
);
assert_eq!(
reports.len(),
floor_len + 1,
"the floor is intact — added to, never displaced"
);
}
#[test]
fn pack_contract_pack_gate_carries_the_run_outcome() {
use crate::gate::Gate;
let pass = PackGate::from_run("g", "cd .", true, String::new());
assert_eq!(pass.kind(), GateKind::Deterministic);
assert!(pass.evaluate().passed());
assert_eq!(pass.evaluate().artefact.reference, "cd .");
let fail = PackGate::from_run("g", "cd .", false, "boom".to_string());
assert!(!fail.evaluate().passed());
assert_eq!(fail.evaluate().artefact.detail.as_deref(), Some("boom"));
assert_eq!(fail.evaluate().score, None, "boolean-only gate");
}
#[test]
fn pack_contract_load_for_config_resolves_repo_relative_and_refuses_non_packs() {
let repo = tempfile::tempdir().unwrap();
let cfg = MissionConfig::default();
assert_eq!(load_for_config(&cfg, repo.path()).unwrap(), None);
let (_tmp, pack_src) = pack_dir_with("[pack]\nname = \"x\"\nschema = 3\n", &[]);
let rel = repo.path().join("my-pack");
std::fs::create_dir_all(&rel).unwrap();
std::fs::copy(pack_src.join(PACK_MANIFEST), rel.join(PACK_MANIFEST)).unwrap();
let cfg = MissionConfig {
pack_dir: Some("my-pack".to_string()),
..MissionConfig::default()
};
let pack = load_for_config(&cfg, repo.path()).unwrap().expect("a pack");
assert_eq!(pack.name, "x");
std::fs::create_dir_all(repo.path().join("not-a-pack")).unwrap();
let cfg = MissionConfig {
pack_dir: Some("not-a-pack".to_string()),
..MissionConfig::default()
};
let err = load_for_config(&cfg, repo.path()).expect_err("must fail");
assert!(err.contains("it is not a pack"), "{err}");
let cfg = MissionConfig {
pack_dir: Some("missing-dir".to_string()),
..MissionConfig::default()
};
let err = load_for_config(&cfg, repo.path()).expect_err("must fail");
assert!(err.contains("is not a directory"), "{err}");
let cfg = MissionConfig {
pack_dir: Some("../pack".to_string()),
..MissionConfig::default()
};
let err = load_for_config(&cfg, repo.path()).expect_err("traversal must fail");
assert!(err.contains("without parent components"), "{err}");
}
#[cfg(unix)]
#[test]
fn pack_textfile_nofollow_refuses_a_symlinked_leaf() {
use std::os::unix::fs::symlink;
let tmp = tempfile::tempdir().unwrap();
let outside_file = tmp.path().join("engine-readable-secret.md");
std::fs::write(&outside_file, "sk-live-secret-value").unwrap();
let pack = tmp.path().join("pack");
std::fs::create_dir_all(pack.join("prompts")).unwrap();
std::fs::write(
pack.join(PACK_MANIFEST),
"[pack]\nname = \"x\"\nschema = 3\n\n[[prompt]]\nname = \"p\"\nrole = \"worker\"\ntextFile = \"prompts/scrutiny.md\"\n",
)
.unwrap();
symlink(&outside_file, pack.join("prompts").join("scrutiny.md")).unwrap();
let err = Pack::load(&pack).expect_err("a symlinked textFile must be refused");
assert!(err.contains("field `textFile`"), "names the field: {err}");
assert!(err.contains("no-follow"), "says why: {err}");
}
#[cfg(unix)]
#[test]
fn pack_textfile_nofollow_refuses_a_symlinked_parent_dir() {
use std::os::unix::fs::symlink;
let tmp = tempfile::tempdir().unwrap();
let outside = tmp.path().join("outside");
std::fs::create_dir_all(&outside).unwrap();
std::fs::write(outside.join("scrutiny.md"), "exfiltrated prompt text").unwrap();
let pack = tmp.path().join("pack");
std::fs::create_dir_all(&pack).unwrap();
std::fs::write(
pack.join(PACK_MANIFEST),
"[pack]\nname = \"x\"\nschema = 3\n\n[[prompt]]\nname = \"p\"\nrole = \"worker\"\ntextFile = \"prompts/scrutiny.md\"\n",
)
.unwrap();
symlink(&outside, pack.join("prompts")).unwrap();
let err = Pack::load(&pack).expect_err("a symlinked parent dir must be refused");
assert!(err.contains("field `textFile`"), "names the field: {err}");
assert!(err.contains("no-follow"), "says why: {err}");
}
#[test]
fn pack_textfile_nofollow_plain_in_pack_file_loads() {
let (_tmp, dir) = pack_dir_with(
"[pack]\nname = \"x\"\nschema = 3\n\n[[prompt]]\nname = \"p\"\nrole = \"worker\"\ntextFile = \"prompts/scrutiny.md\"\n",
&[("prompts/scrutiny.md", "zz plain in-pack text\n")],
);
let pack = Pack::load(&dir).expect("load").expect("a pack");
assert_eq!(pack.prompts[0].text, "zz plain in-pack text\n");
assert_eq!(
pack.prompts[0].source,
PromptSource::File("prompts/scrutiny.md".to_string())
);
}
}