1#[derive(Clone, Debug, PartialEq, Eq)]
5pub struct SelfHostingScenario {
6 pub id: &'static str,
8 pub title: &'static str,
10 pub runner_mode: &'static str,
12 pub live_model: bool,
14 pub network: bool,
16 pub evidence: &'static [&'static str],
18 pub roles: &'static [&'static str],
20 pub cassette_events: &'static [&'static str],
22 pub cassette_hash: &'static str,
24}
25
26pub 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
141pub 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
168pub 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}