Skip to main content

sim_lib_agent/atelier/
self_hosting.rs

1//! Deterministic self-hosting scenario fixtures for the Atelier.
2
3/// One self-hosting scenario exercised by recipes and conformance tests.
4#[derive(Clone, Debug, PartialEq, Eq)]
5pub struct SelfHostingScenario {
6    /// Stable scenario id, matching the recipe directory.
7    pub id: &'static str,
8    /// Scenario title.
9    pub title: &'static str,
10    /// Runner mode used by the scenario.
11    pub runner_mode: &'static str,
12    /// Whether a live model is used.
13    pub live_model: bool,
14    /// Whether network access is used.
15    pub network: bool,
16    /// Required evidence families.
17    pub evidence: &'static [&'static str],
18    /// Roles participating in the scenario.
19    pub roles: &'static [&'static str],
20    /// Cassette event payloads used for the replay hash.
21    pub cassette_events: &'static [&'static str],
22    /// Expected replay hash for `cassette_events`.
23    pub cassette_hash: &'static str,
24}
25
26/// Returns the Atelier self-hosting scenarios used by recipe and cassette tests.
27pub fn self_hosting_scenarios() -> Vec<SelfHostingScenario> {
28    vec![
29        SelfHostingScenario {
30            id: "atelier-radar-standard-crate",
31            title: "Retrieval Radar standard crate",
32            runner_mode: "fake",
33            live_model: false,
34            network: false,
35            evidence: &["radar", "ranked-hints", "confidence"],
36            roles: &["cartographer"],
37            cassette_events: &[
38                "fake-runner:load-index:sim-kernel",
39                "radar-query:standard-crate-operations",
40                "rank:hints:3",
41                "explain:confidence:0.91",
42            ],
43            cassette_hash: "fnv1a64:7f9f5b6c744aa528",
44        },
45        SelfHostingScenario {
46            id: "atelier-runtime-operation",
47            title: "Runtime operation edit loop",
48            runner_mode: "fake",
49            live_model: false,
50            network: false,
51            evidence: &[
52                "runtime-operation",
53                "rustdoc",
54                "codec-prism",
55                "generated-docs",
56                "validation",
57                "pin-plan",
58            ],
59            roles: &["editor", "validator", "docs-agent", "pin-agent"],
60            cassette_events: &[
61                "fake-runner:open-runtime-op-descriptor",
62                "editor:add-operation:sample-op",
63                "codec-prism:lisp-json-roundtrip",
64                "validate:meta-check",
65                "pin-plan:sim-runtime",
66            ],
67            cassette_hash: "fnv1a64:8a3282f3c7ef34b2",
68        },
69        SelfHostingScenario {
70            id: "atelier-codec-roundtrip",
71            title: "Codec round-trip fixture",
72            runner_mode: "fake",
73            live_model: false,
74            network: false,
75            evidence: &["codec-prism", "roundtrip", "semantic-id", "simdoc"],
76            roles: &["editor", "validator"],
77            cassette_events: &[
78                "fake-runner:load-codec-fixture",
79                "codec-prism:lisp-json-algol-roundtrip",
80                "assert:semantic-id-stable",
81                "docs:simdoc-check",
82            ],
83            cassette_hash: "fnv1a64:32be093d6f32315a",
84        },
85        SelfHostingScenario {
86            id: "atelier-guideline-firewall",
87            title: "Guideline Firewall bad fixture",
88            runner_mode: "fake",
89            live_model: false,
90            network: false,
91            evidence: &["guideline-firewall", "rule-evidence", "refused-capability"],
92            roles: &["guard", "reviewer"],
93            cassette_events: &[
94                "fake-runner:load-bad-fixture",
95                "guard:present-tense-public-docs",
96                "refuse:EditRepo(sim-web)",
97                "evidence:past-tense-wording",
98            ],
99            cassette_hash: "fnv1a64:768ad342812bdd9a",
100        },
101        SelfHostingScenario {
102            id: "atelier-change-capsule",
103            title: "Multi-agent Change Capsule",
104            runner_mode: "cassette",
105            live_model: false,
106            network: false,
107            evidence: &[
108                "change-capsule",
109                "validation",
110                "docs",
111                "pin-plan",
112                "human-gate",
113                "replay-hash",
114            ],
115            roles: &[
116                "cartographer",
117                "editor",
118                "guard",
119                "validator",
120                "docs-agent",
121                "pin-agent",
122                "reviewer",
123                "human-gate",
124            ],
125            cassette_events: &[
126                "cassette-runner:cartographer-scope",
127                "fake-runner:editor-patch",
128                "fake-runner:guard-approve",
129                "process:validator-pass",
130                "process:docs-agent-pass",
131                "fake-runner:pin-agent-plan",
132                "fake-runner:reviewer-summary",
133                "human-gate:approved",
134                "replay:hash-match",
135            ],
136            cassette_hash: "fnv1a64:5ec7c4222478f8f1",
137        },
138    ]
139}
140
141/// Validate offline runner, evidence, and replay-hash invariants.
142pub fn validate_self_hosting_scenarios(scenarios: &[SelfHostingScenario]) -> Vec<String> {
143    let mut failures = Vec::new();
144    for scenario in scenarios {
145        if scenario.runner_mode != "fake" && scenario.runner_mode != "cassette" {
146            failures.push(format!("{} uses unsupported runner", scenario.id));
147        }
148        if scenario.live_model {
149            failures.push(format!("{} uses a live model", scenario.id));
150        }
151        if scenario.network {
152            failures.push(format!("{} uses network access", scenario.id));
153        }
154        if scenario.evidence.is_empty() {
155            failures.push(format!("{} has no evidence", scenario.id));
156        }
157        let actual_hash = cassette_content_hash(scenario.cassette_events);
158        if actual_hash != scenario.cassette_hash {
159            failures.push(format!(
160                "{} cassette hash mismatch: expected {}, got {}",
161                scenario.id, scenario.cassette_hash, actual_hash
162            ));
163        }
164    }
165    failures
166}
167
168/// Stable FNV-1a hash used by self-hosting cassette fixtures.
169pub fn cassette_content_hash(events: &[&str]) -> String {
170    let mut hash = 0xcbf29ce484222325u64;
171    for event in events {
172        for byte in event.as_bytes() {
173            hash ^= u64::from(*byte);
174            hash = hash.wrapping_mul(0x100000001b3);
175        }
176    }
177    format!("fnv1a64:{hash:016x}")
178}