1use crate::catalog::CatalogEntry;
25use kernel::{AdmissionRequirement, LandingPolicy, Mode, RunPolicy};
26
27pub const DEV_ENTRY: &str = "dev";
29
30#[must_use]
40pub fn dev_entry() -> CatalogEntry {
41 CatalogEntry {
42 name: DEV_ENTRY.to_owned(),
43 disclosure: "when the work is to change this city's own code, SPECs or tools, open this \
44 entry before you touch anything"
45 .to_owned(),
46 expansion: "This city is built from small components, each with its SPEC beside it at \
47 `crates/<crate>/<crate>-SPEC.md`. Read that SPEC, then the code, then the \
48 tests next to the code - in that order, and before you change any of them. \
49 Where the implementation would differ from the SPEC, the SPEC changes first \
50 and says why.\n\nThe person chooses what a change must prove before it is \
51 merged:\n- tested: the asset's own tests ran and passed.\n- contract_kept: \
52 the asset's observable contract did not move.\n- double_validated: the change \
53 holds on held-in and on held-out evidence.\nAnd where it lands: \
54 ordinary work takes the building's own road, while an experiment works in a \
55 tree of its own and nothing in it is merged.\n\nYour next step: say which \
56 evidence this work needs and why, and wait for the person to dispatch it \
57 with that requirement. Choosing a requirement does not provide the evidence."
58 .to_owned(),
59 hash: None,
62 package: None,
63 }
64}
65
66#[must_use]
70pub fn catalog_entry(mode: Mode) -> CatalogEntry {
71 let (disclosure, expansion) = match mode {
72 Mode::Chat => (
73 "chat mode: focus on conversing with the person; answer what they said, in their language",
74 "Reply in the conversation. Start work, plans or dispatches only when the person asks for them.",
75 ),
76 Mode::Work => (
77 "work mode: carry out the task towards the stated goal; report when the goal is met",
78 "When the task asks for a plan first, write it into Roadmap.md with the `plan` tool \
79 before you change anything, then work through it. Report what you did and the \
80 evidence that the goal is met.",
81 ),
82 };
83 CatalogEntry {
84 name: format!("mode:{}", mode.as_str()),
85 disclosure: disclosure.to_owned(),
86 expansion: expansion.to_owned(),
87 hash: None,
88 package: None,
89 }
90}
91
92#[must_use]
110pub fn core_tools(mode: Mode) -> &'static [&'static str] {
111 match mode {
112 Mode::Chat => &[
113 crate::tools::CallTool::NAME,
114 crate::tools::DescribeTool::NAME,
115 crate::tools::ReadTool::NAME,
116 crate::tools::SearchTool::NAME,
117 crate::tools::StatusTool::NAME,
118 ],
119 Mode::Work => &[
120 crate::tools::CallTool::NAME,
121 crate::tools::DescribeTool::NAME,
122 crate::tools::EditTool::NAME,
123 kernel::ToolName::EXEC,
124 crate::tools::ReadTool::NAME,
125 crate::tools::SearchTool::NAME,
126 crate::tools::StatusTool::NAME,
127 ],
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
132pub struct Produced {
133 pub tests_passed: Option<bool>,
135 pub contract_moved: bool,
137 pub held_in: Option<bool>,
139 pub held_out: Option<bool>,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum Admission {
146 Lands,
147 Refused {
148 because: &'static str,
149 alternative: &'static str,
150 },
151}
152
153#[must_use]
161pub fn admits(policy: &RunPolicy, produced: &Produced) -> Admission {
162 match policy.landing {
163 LandingPolicy::Experiment => Admission::Refused {
164 because: "nothing produced in an experiment lands",
165 alternative: "write what you learned into Memo.md, then ask for the work again \
166 with the ordinary landing",
167 },
168 LandingPolicy::Ordinary => admits_evidence(policy.admit, produced),
169 }
170}
171
172fn admits_evidence(required: AdmissionRequirement, produced: &Produced) -> Admission {
182 match required {
183 AdmissionRequirement::Standing => Admission::Lands,
184 AdmissionRequirement::Tested => match produced.tests_passed {
185 Some(true) => Admission::Lands,
186 Some(false) => Admission::Refused {
187 because: "the asset's own tests did not pass",
188 alternative: "fix the asset or the test, then offer it again",
189 },
190 None => Admission::Refused {
191 because: "the asset has no tests of its own",
192 alternative: "write the test that would fail if this asset broke",
193 },
194 },
195 AdmissionRequirement::ContractKept => {
196 if produced.contract_moved {
197 Admission::Refused {
198 because: "the asset's observable contract moved",
199 alternative: "keep the contract and renovate behind it, or ask for the work \
200 again under double_validated with held-out evidence",
201 }
202 } else {
203 Admission::Lands
204 }
205 }
206 AdmissionRequirement::DoubleValidated => match (produced.held_in, produced.held_out) {
207 (Some(true), Some(true)) => Admission::Lands,
208 (Some(false), _) => Admission::Refused {
209 because: "it got worse on the held-in set",
210 alternative: "a change that costs more than it buys is not adopted; revise it",
211 },
212 (_, Some(false)) => Admission::Refused {
213 because: "it did not stand up on the held-out set",
214 alternative: "a gain that only shows where it was built is a gain in fitting, \
215 not in ability",
216 },
217 _ => Admission::Refused {
218 because: "one half of the double validation is missing",
219 alternative: "run the suite on both sets; an unmeasured change is not adopted on \
220 confidence",
221 },
222 },
223 }
224}
225
226#[cfg(test)]
227#[allow(
228 clippy::unwrap_used,
229 clippy::expect_used,
230 clippy::panic,
231 reason = "test code"
232)]
233mod tests {
234 use super::*;
235
236 fn work(admit: AdmissionRequirement) -> RunPolicy {
237 RunPolicy {
238 admit,
239 ..RunPolicy::of(Mode::Work)
240 }
241 }
242
243 #[test]
246 fn a_work_run_without_the_evidence_it_chose_does_not_land() {
247 let Admission::Refused { because, .. } =
248 admits(&work(AdmissionRequirement::Tested), &Produced::default())
249 else {
250 panic!("work that chose `tested` and ran no test landed");
251 };
252 assert!(because.contains("no tests"), "{because}");
253 }
254
255 #[test]
257 fn chat_is_one_line_about_the_conversation() {
258 let entry = catalog_entry(Mode::Chat);
259 assert_eq!(entry.name, "mode:chat");
260 assert!(entry.disclosure.contains("convers"), "{}", entry.disclosure);
261 assert!(!entry.disclosure.contains('\n'), "one line");
262 assert!(
263 !entry.expansion.is_empty(),
264 "a read of the entry says something"
265 );
266 assert_eq!(catalog_entry(Mode::Work).name, "mode:work");
267 }
268
269 #[test]
272 fn standing_adds_nothing_in_either_mode() {
273 for mode in Mode::ALL {
274 assert_eq!(
275 admits(&RunPolicy::of(mode), &Produced::default()),
276 Admission::Lands
277 );
278 }
279 }
280
281 #[test]
282 fn tested_wants_the_asset_proven_and_says_which_way_it_failed() {
283 let policy = work(AdmissionRequirement::Tested);
284 let failed = Produced {
285 tests_passed: Some(false),
286 ..Produced::default()
287 };
288 let Admission::Refused { because, .. } = admits(&policy, &failed) else {
289 panic!("a failing asset does not land");
290 };
291 assert!(
292 because.contains("did not pass"),
293 "not the untested sentence"
294 );
295 let passed = Produced {
296 tests_passed: Some(true),
297 ..Produced::default()
298 };
299 assert_eq!(admits(&policy, &passed), Admission::Lands);
300 }
301
302 #[test]
303 fn contract_kept_refuses_a_renovation_that_moved_the_contract() {
304 let policy = work(AdmissionRequirement::ContractKept);
305 assert_eq!(admits(&policy, &Produced::default()), Admission::Lands);
306 let moved = Produced {
307 contract_moved: true,
308 ..Produced::default()
309 };
310 let Admission::Refused { alternative, .. } = admits(&policy, &moved) else {
311 panic!("a moved contract is not a renovation");
312 };
313 assert!(
314 alternative.contains("double_validated"),
315 "the refusal names the requirement that would take it"
316 );
317 }
318
319 #[test]
320 fn double_validated_takes_nothing_on_confidence() {
321 let policy = work(AdmissionRequirement::DoubleValidated);
322 let both = Produced {
323 held_in: Some(true),
324 held_out: Some(true),
325 ..Produced::default()
326 };
327 assert_eq!(admits(&policy, &both), Admission::Lands);
328 for missing in [
329 Produced {
330 held_in: Some(true),
331 ..Produced::default()
332 },
333 Produced {
334 held_out: Some(true),
335 ..Produced::default()
336 },
337 Produced::default(),
338 ] {
339 assert!(
340 matches!(admits(&policy, &missing), Admission::Refused { .. }),
341 "half of a double validation is not a double validation"
342 );
343 }
344 let regressed = Produced {
345 held_in: Some(true),
346 held_out: Some(false),
347 ..Produced::default()
348 };
349 let Admission::Refused { because, .. } = admits(&policy, ®ressed) else {
350 panic!("a change that only holds where it was built does not land");
351 };
352 assert!(because.contains("held-out"));
353 }
354
355 #[test]
358 fn an_experiment_lands_nothing_however_well_it_went() {
359 let excellent = Produced {
360 tests_passed: Some(true),
361 contract_moved: false,
362 held_in: Some(true),
363 held_out: Some(true),
364 };
365 for admit in AdmissionRequirement::ALL {
366 let policy = RunPolicy {
367 landing: LandingPolicy::Experiment,
368 write: kernel::WriteLimit::Create,
369 ..work(admit)
370 };
371 assert!(matches!(
372 admits(&policy, &excellent),
373 Admission::Refused { .. }
374 ));
375 }
376 }
377}