use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use ikigai_conformance::{rdf, Check, Fixture, Report, Suite};
use ikigai_core::{Kernel, Verb};
const OWN: &[(&str, &str)] = &[
("about", "ikigai-embedded"),
("agent-select", "ikigai-embedded"),
("alias-demo", "ikigai-embedded"),
("calendar-request", "ikigai-embedded"),
("catalog-cards-xsl", "ikigai-embedded"),
("client", "ikigai-embedded"),
("client-issue", "ikigai-embedded"),
("clock-now", "ikigai-embedded"),
("contact-block", "ikigai-embedded"),
("contactblock-link", "ikigai-embedded"),
("control", "ikigai-embedded"),
("decide-accept", "ikigai-embedded"),
("decide-link", "ikigai-embedded"),
("decisions", "ikigai-embedded"),
("foaf", "ikigai-embedded"),
("greeter", "ikigai-embedded"),
("host-demo", "ikigai-embedded"),
("host-heartbeat", "ikigai-embedded"),
("host-history", "ikigai-embedded"),
("host-identity", "ikigai-embedded"),
("host-info", "ikigai-embedded"),
("kernel-health", "ikigai-embedded"),
("lisp-aliases", "ikigai-embedded"),
("page", "ikigai-embedded"),
("passkey-challenge", "ikigai-embedded"),
("passkey-enroll-open", "ikigai-embedded"),
("passkey-js", "ikigai-embedded"),
("passkey-register", "ikigai-embedded"),
("peer-list", "ikigai-embedded"),
("people", "ikigai-embedded"),
("booking", "ikigai-intake"),
("contact", "ikigai-intake"),
("send", "ikigai-email"),
("space", "ikigai-intray"),
("time-cancel", "ikigai-time"),
("time-jobs", "ikigai-time"),
("time-schedule", "ikigai-time"),
("tz-convert", "ikigai-tz"),
("tz-now", "ikigai-tz"),
("view-derive", "ikigai-view"),
("view-derive-tick", "ikigai-view"),
("view-ingest", "ikigai-view"),
];
const INHERITED: &[(&str, &str)] = &[
("bookmarks", "ikigai-cms"),
("compose", "ikigai-fn"),
("conditional", "ikigai-fn"),
("echo", "ikigai-fn"),
("greet", "ikigai-fn"),
("reverseList", "ikigai-fn"),
("split", "ikigai-fn"),
("toUpper", "ikigai-fn"),
("wrap", "ikigai-fn"),
("availability", "ikigai-personal"),
("calendar", "ikigai-personal"),
("calendar-config", "ikigai-personal"),
("calendars", "ikigai-personal"),
("contacts", "ikigai-personal"),
("eval", "ikigai-lisp"),
("file", "ikigai-fs"),
("httpDelete", "ikigai-http"),
("httpGet", "ikigai-http"),
("httpHead", "ikigai-http"),
("httpPatch", "ikigai-http"),
("httpPost", "ikigai-http"),
("httpPut", "ikigai-http"),
("ikigai-vocab", "ikigai-vocab"),
("jsonld-compact", "ikigai-jsonld"),
("jsonld-expand", "ikigai-jsonld"),
("jsonld-flatten", "ikigai-jsonld"),
("llm-ask", "ikigai-llm"),
("llm-config", "ikigai-llm"),
("llm-models", "ikigai-llm"),
("llm-ollama-ask", "ikigai-llm"),
("llm-ollama-installed", "ikigai-llm"),
("llm-ollama-model", "ikigai-llm"),
("llm-ollama-up", "ikigai-llm"),
("llm-select", "ikigai-llm"),
("org-agenda", "ikigai-org"),
("rdf-diff", "ikigai-rdf"),
("rdf-from-sexpr", "ikigai-sexpr"),
("rdf-transrept", "ikigai-rdf"),
("rdf-union", "ikigai-rdf"),
("repo-branch", "ikigai-repo"),
("repo-list", "ikigai-repo"),
("repo-log", "ikigai-repo"),
("repo-pr-checks", "ikigai-repo"),
("repo-pr-diff", "ikigai-repo"),
("repo-pr-files", "ikigai-repo"),
("repo-pr-list", "ikigai-repo"),
("repo-pr-view", "ikigai-repo"),
("repo-status", "ikigai-repo"),
("system-exec", "ikigai-repo"),
("sexpr-from-rdf", "ikigai-sexpr"),
("sexpr-to-rdf", "ikigai-sexpr"),
("shacl-validate", "ikigai-shacl"),
("sign", "ikigai-sign"),
("verify", "ikigai-sign"),
("encrypt", "ikigai-encrypt"),
("decrypt", "ikigai-encrypt"),
("sniff", "ikigai-sniff"),
("transrept-auto", "ikigai-sniff"),
("sparql-ask", "ikigai-sparql"),
("sparql-construct", "ikigai-sparql"),
("sparql-describe", "ikigai-sparql"),
("sparql-from-sexpr", "ikigai-sexpr"),
("sparql-select", "ikigai-sparql"),
("urn:meeting:zoom:schedule", "ikigai-meeting"),
("urn:secret", "ikigai-secret"),
("secret-generate", "ikigai-secret"),
("secret-unlock", "ikigai-secret"),
("grep", "ikigai-text"),
("head", "ikigai-text"),
("nl", "ikigai-text"),
("rev", "ikigai-text"),
("sort", "ikigai-text"),
("tail", "ikigai-text"),
("uniq", "ikigai-text"),
("wc", "ikigai-text"),
("xslt-transform", "ikigai-xslt"),
];
const OWNER_ONLY: &[&str] = &[
"contacts",
"calendar",
"availability",
"calendars",
"calendar-config",
"eval",
"system-exec",
"repo-status",
"urn:secret",
"secret-generate",
"secret-unlock",
"send",
"urn:meeting:zoom:schedule",
"client-issue",
"peer-list",
];
fn fixture_home() -> &'static Path {
static HOME: OnceLock<PathBuf> = OnceLock::new();
HOME.get_or_init(|| {
let dir = std::env::temp_dir().join(format!(
"ikigai-embedded-conformance-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("config/ikigai")).expect("config home");
std::fs::create_dir_all(dir.join("workspace")).expect("workspace");
std::fs::create_dir_all(dir.join("org")).expect("org dir");
std::fs::write(
dir.join("org/agenda.org"),
"* TODO a fixture task\n SCHEDULED: <2026-01-01 Thu>\n",
)
.expect("org file");
std::fs::write(
dir.join("config/ikigai/calendar.json"),
format!(
r#"{{"view":"Conformance-View","org_dir":"{}","org_files":["agenda.org"]}}"#,
dir.join("org").display()
),
)
.expect("calendar.json");
std::env::set_var("HOME", &dir);
std::env::set_var("XDG_CONFIG_HOME", dir.join("config"));
ikigai_embedded::set_file_root(dir.join("workspace"));
seed_workspace(&dir.join("workspace"));
dir
})
.as_path()
}
fn seed_workspace(root: &Path) {
use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
use ed25519_dalek::pkcs8::{EncodePrivateKey, EncodePublicKey};
use ed25519_dalek::SigningKey;
let key = SigningKey::from_bytes(&[9u8; 32]);
let public = key
.verifying_key()
.to_public_key_pem(LineEnding::LF)
.expect("SPKI PEM");
for name in ["decide.pub", "contact-block.pub"] {
std::fs::write(root.join(name), &public).expect("verifying key");
}
std::fs::write(
root.join("contact-block.key"),
key.to_pkcs8_pem(LineEnding::LF)
.expect("PKCS8 PEM")
.as_str(),
)
.expect("signing key");
std::fs::create_dir_all(root.join("clients")).expect("clients dir");
std::fs::write(
root.join("clients/conformance.json"),
r#"{"id":"conformance","name":"Conformance Fixture"}"#,
)
.expect("client record");
std::fs::write(root.join("conformance.txt"), "a fixture document\n").expect("file fixture");
std::fs::write(root.join("conformance.xsl"), XSL).expect("stylesheet fixture");
std::fs::write(root.join("conformance.xml"), "<doc/>").expect("xml fixture");
}
fn root_kernel() -> Kernel {
fixture_home();
ikigai_embedded::kernel()
}
fn http_kernel() -> Kernel {
fixture_home();
ikigai_embedded::kernel_for("conformance")
}
const TURTLE: &str = "<http://example.org/a> <http://purl.org/dc/terms/title> \"demo\" .\n";
const JSON_LD: &str = r#"{"@context":{"title":"http://purl.org/dc/terms/title"},"@id":"http://example.org/a","title":"demo"}"#;
const JSON_LD_CONTEXT: &str = r#"{"title":"http://purl.org/dc/terms/title"}"#;
const XSL: &str = r#"<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="xml"/><xsl:template match="/"><ok/></xsl:template></xsl:stylesheet>"#;
fn base() -> Suite {
Suite::new()
.fixture(
Fixture::new("rdf-union", Verb::Source)
.arg("content", TURTLE)
.arg("with", TURTLE),
)
.fixture(
Fixture::new("rdf-diff", Verb::Source)
.arg("content", TURTLE)
.arg("with", TURTLE)
.arg("mode", "added"),
)
.fixture(
Fixture::new("rdf-transrept", Verb::Source)
.arg("content", TURTLE)
.arg("as", "application/n-triples"),
)
.fixture(Fixture::new("jsonld-expand", Verb::Source).arg("content", JSON_LD))
.fixture(Fixture::new("jsonld-flatten", Verb::Source).arg("content", JSON_LD))
.fixture(
Fixture::new("jsonld-compact", Verb::Source)
.arg("content", JSON_LD)
.arg("context", JSON_LD_CONTEXT),
)
.fixture(
Fixture::new("xslt-transform", Verb::Source)
.arg("src", "urn:file:conformance.xml")
.arg("stylesheet", "urn:file:conformance.xsl")
.arg("as", "text/html"),
)
.fixture(Fixture::new("file", Verb::Source).binding("path", "conformance.txt"))
.fixture(Fixture::new("space", Verb::Source).binding("name", "conformance"))
.fixture(Fixture::new("client", Verb::Source).binding("token", "conformance"))
.fixture(Fixture::new("contact", Verb::Sink).arg(
"content",
"name=Conformance&email=someone%40example.org&message=a+fixture+enquiry",
))
.fixture(Fixture::new("booking", Verb::Sink).arg(
"content",
"name=Conformance&email=someone%40example.org&period=week&zone=UTC",
))
.fixture(
Fixture::new("tz-convert", Verb::Source)
.arg("in", "2026-01-01T12:00:00Z")
.arg("from", "UTC")
.arg("to", "America/New_York"),
)
.pure("page")
.pure("control")
.pure("about")
.pure("catalog-cards-xsl")
.pure("tz-convert")
.pure("clock-now")
.pure("tz-now")
.opt_out("httpGet", None, "outbound HTTP over a real transport")
.opt_out("httpHead", None, "outbound HTTP over a real transport")
.opt_out("httpPost", None, "outbound HTTP over a real transport")
.opt_out("httpPut", None, "outbound HTTP over a real transport")
.opt_out("httpPatch", None, "outbound HTTP over a real transport")
.opt_out("httpDelete", None, "outbound HTTP over a real transport")
}
fn root_suite() -> Suite {
base()
.fixture(Fixture::new("sniff", Verb::Source).arg("content", TURTLE))
.fixture(Fixture::new("transrept-auto", Verb::Source).arg("content", TURTLE))
.fixture(
Fixture::new("sparql-select", Verb::Source)
.arg("query", "SELECT * WHERE { ?s ?p ?o } LIMIT 1"),
)
.fixture(Fixture::new("sparql-ask", Verb::Source).arg("query", "ASK { ?s ?p ?o }"))
.fixture(
Fixture::new("sparql-describe", Verb::Source).arg("query", "DESCRIBE <urn:demo:a>"),
)
.fixture(
Fixture::new("sparql-construct", Verb::Source)
.arg("query", "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o } LIMIT 1"),
)
.fixture(
Fixture::new("contactblock-link", Verb::Source).arg("email", "someone@example.org"),
)
.opt_out_check(
"kernel-health",
Check::Vocabulary,
"ik:Health / ik:verdict / ik:uptimeSeconds / ik:staleJobs are undefined in ikigai-vocab (at the pin AND at HEAD); the fix is vocabulary.ttl in ikigai-core plus a /ns deploy. The exact undefined set is pinned by hand below.",
)
.opt_out("system-exec", None, "spawns a subprocess")
.opt_out("eval", None, "evaluates arbitrary Steel code")
.opt_out("repo-status", None, "runs git in the invoking working tree")
.opt_out("repo-log", None, "runs git in the invoking working tree")
.opt_out("repo-branch", None, "runs git in the invoking working tree")
.opt_out("repo-list", None, "enumerates repositories on the machine")
.opt_out(
"repo-pr-checks",
None,
"shells out to `gh`: network and auth",
)
.opt_out("repo-pr-view", None, "shells out to `gh`: network and auth")
.opt_out("repo-pr-list", None, "shells out to `gh`: network and auth")
.opt_out("repo-pr-diff", None, "shells out to `gh`: network and auth")
.opt_out("repo-pr-files", None, "shells out to `gh`: network and auth")
.opt_out("llm-ask", None, "POSTs to a live inference server")
.opt_out("llm-ollama-ask", None, "POSTs to a live inference server")
.opt_out("llm-ollama-up", None, "probes a live inference server")
.opt_out(
"llm-ollama-installed",
None,
"queries a live inference server",
)
.opt_out("llm-ollama-model", None, "queries a live inference server")
.opt_out("llm-models", None, "may discover over the network")
.opt_out("llm-select", None, "may discover over the network")
.opt_out("peer-list", None, "mDNS multicast on the local network")
.opt_out("send", None, "submits real mail over SMTP")
.opt_out(
"urn:meeting:zoom:schedule",
None,
"creates a real Zoom meeting through the provider API",
)
.opt_out(
"decide-accept",
None,
"runs booking confirmation: schedules and emails downstream",
)
.opt_out(
"client-issue",
None,
"mints a durable client credential, and `send=` emails it",
)
.opt_out(
"contacts",
None,
"reads the macOS contact store (EventKit/TCC)",
)
.opt_out(
"calendar",
None,
"reads and WRITES the macOS calendar (EventKit/TCC)",
)
.opt_out(
"calendars",
None,
"reads and writes the macOS calendar set (EventKit/TCC)",
)
.opt_out(
"calendar-config",
None,
"reads the macOS calendar set (EventKit/TCC)",
)
.opt_out(
"availability",
None,
"reads the macOS calendar (EventKit/TCC)",
)
.opt_out(
"view-derive",
None,
"writes the derived calendar through EventKit",
)
.opt_out(
"view-derive-tick",
None,
"writes the derived calendar through EventKit",
)
.opt_out(
"view-ingest",
None,
"writes the derived calendar through EventKit",
)
.opt_out(
"urn:secret",
None,
"macOS Keychain, behind a Touch ID prompt",
)
.opt_out(
"secret-unlock",
None,
"macOS Keychain, behind a Touch ID prompt",
)
.opt_out(
"secret-generate",
None,
"mints a real key into the macOS Keychain, behind a Touch ID prompt",
)
.opt_out(
"time-schedule",
None,
"registers a real job on the process-global registry",
)
.opt_out(
"time-cancel",
None,
"cancels real jobs on the process-global registry",
)
.opt_out(
"decide-link",
None,
"reads the signing key through urn:secret:*: Keychain, behind a Touch ID prompt",
)
}
fn door_suite() -> Suite {
base()
.fixture(Fixture::new("calendar-request", Verb::Source).binding("action", "decline"))
.opt_out("foaf", None, "issues urn:httpGet: reaches the network")
.opt_out(
"passkey-register",
Some(Verb::Sink),
"refuses with a typed Denied on a STATE gate (no enrollment window), which \
ENFORCED reads as an undeclared capability; there is no capability to declare",
)
}
fn walked(kernel: &Kernel) -> BTreeMap<String, String> {
kernel
.entries()
.expect("an enumerable root")
.iter()
.filter(|e| !e.pattern.starts_with("urn:kernel:"))
.map(|e| {
let id = kernel
.describe_pattern(&e.pattern)
.unwrap_or_else(|| panic!("`{}` describes itself", e.pattern))
.id;
(e.pattern.clone(), id)
})
.collect()
}
fn owners() -> BTreeMap<&'static str, &'static str> {
OWN.iter().chain(INHERITED).copied().collect()
}
fn by_owner(report: &Report) -> BTreeMap<&'static str, usize> {
let owners = owners();
let mut tally: BTreeMap<&'static str, usize> = BTreeMap::new();
for finding in &report.findings {
if let Some(owner) = owners.get(finding.endpoint.as_str()) {
*tally.entry(*owner).or_default() += 1;
}
}
tally
}
#[test]
fn the_walked_catalog_is_classified() {
let known: BTreeSet<&str> = owners().keys().copied().collect();
let own: BTreeSet<&str> = OWN.iter().map(|(id, _)| *id).collect();
let mut seen: BTreeSet<String> = BTreeSet::new();
for kernel in [root_kernel(), http_kernel()] {
for (pattern, id) in walked(&kernel) {
assert!(
known.contains(id.as_str()),
"`{pattern}` describes itself as `{id}`, which is in neither OWN nor \
INHERITED: classify it (and if it is ours, its inputs need classes)"
);
seen.insert(id);
}
}
let listed: BTreeSet<&str> = known.iter().copied().collect();
let unseen: Vec<&&str> = listed
.iter()
.filter(|id| !seen.contains(**id))
.collect::<Vec<_>>();
assert!(
unseen.is_empty(),
"these ids are classified but no longer bound — drop them from the tables: {unseen:?}"
);
assert!(
own.len() >= 40,
"OWN lost entries without the catalog shrinking: {}",
own.len()
);
}
#[test]
fn conforms() {
let mut ours: Vec<String> = Vec::new();
let own: BTreeSet<&str> = OWN.iter().map(|(id, _)| *id).collect();
for (label, kernel, suite) in [
(
"ikigai_embedded::kernel() — the REPL/TUI root",
root_kernel(),
root_suite(),
),
(
"ikigai_embedded::kernel_for() — the HTTP door",
http_kernel(),
door_suite(),
),
] {
let report = suite.run_blocking(&kernel);
eprintln!("--- {label} ---\n{report}");
eprintln!("findings by owning crate: {:?}", by_owner(&report));
let owners = owners();
let unattributed: Vec<String> = report
.findings
.iter()
.filter(|f| !owners.contains_key(f.endpoint.as_str()))
.map(|f| format!("{} {}", f.endpoint, f.check.label()))
.collect();
assert!(
unattributed.is_empty(),
"every finding must name a classified endpoint; these do not: {unattributed:?}"
);
ours.extend(
report
.findings
.iter()
.filter(|f| own.contains(f.endpoint.as_str()))
.map(|f| format!("[{label}] {} {} {}", f.endpoint, f.check.label(), f.detail)),
);
assert_eq!(
report.checks.skipped().count(),
0,
"every check runs over a composing host: {report}"
);
}
assert!(
ours.is_empty(),
"findings against endpoints this workspace owns:\n{}",
ours.join("\n")
);
}
#[test]
fn a_clock_derived_result_expires_rather_than_threading() {
use ikigai_core::{Expiry, Iri, Request, Verb};
for (label, kernel) in [("root", root_kernel()), ("http door", http_kernel())] {
for target in ["urn:time:now", "urn:tz:now"] {
let repr = futures::executor::block_on(kernel.issue(
Request::new(Verb::Source, Iri::parse(target.to_string()).expect("iri")),
&ikigai_core::Capability::root(),
))
.unwrap_or_else(|e| panic!("{label}: {target}: {e}"));
assert!(
matches!(repr.expiry, Expiry::At(_)),
"{label}: `{target}` must expire at a deadline, not be cached forever or be \
live: {:?}",
repr.expiry
);
assert!(
kernel.is_cached(
&Request::new(Verb::Source, Iri::parse(target.to_string()).expect("iri")),
&ikigai_core::Capability::root(),
),
"{label}: `{target}` is not cached — this kernel has no clock, so every \
`Expiry::At` result on it (including every `urn:httpGet` max-age) \
recomputes on every request"
);
}
}
}
#[test]
fn the_http_door_serves_no_owner_only_resource() {
let door: BTreeSet<String> = walked(&http_kernel()).into_values().collect();
let root: BTreeSet<String> = walked(&root_kernel()).into_values().collect();
for id in OWNER_ONLY {
assert!(
root.contains(*id),
"`{id}` is not bound on the REPL root either: this list has gone stale"
);
assert!(
!door.contains(*id),
"`{id}` is bound on the PUBLIC HTTP door: authority gates it, but it should not \
be reachable there at all"
);
}
for id in [
"foaf",
"contact",
"booking",
"passkey-challenge",
"decisions",
] {
assert!(door.contains(id), "`{id}` is missing from the HTTP door");
}
}
#[test]
fn the_health_graph_uses_exactly_four_undefined_terms() {
use ikigai_core::{Capability, Iri, Request, Verb};
let kernel = root_kernel();
let repr = futures::executor::block_on(
kernel.issue(
Request::new(
Verb::Source,
Iri::parse("urn:host:health".to_string()).expect("iri"),
)
.with_arg("as", ikigai_core::ArgRef::Inline(b"text/turtle".to_vec())),
&Capability::root(),
),
)
.expect("the health graph face resolves");
let triples =
rdf::parse(&repr.repr_type.media_type, &repr.bytes).expect("the face parses as Turtle");
let undefined: Vec<String> = rdf::terms(&triples)
.into_iter()
.filter(|t| !rdf::is_defined(t, &[]))
.collect();
assert_eq!(
undefined,
vec![
"https://ikigai-rs.dev/ns#Health",
"https://ikigai-rs.dev/ns#staleJobs",
"https://ikigai-rs.dev/ns#uptimeSeconds",
"https://ikigai-rs.dev/ns#verdict",
],
"the waived vocabulary exception has changed — widen the waiver's reason, or drop \
both it and this test if `vocabulary.ttl` now defines these"
);
}