1use std::sync::Arc;
2
3use sim_codec_bridge::content_id_string;
4use sim_kernel::{
5 AbiVersion, Args, Callable, Cx, Datum, DatumStore, Error, Export, Expr, Lib, LibManifest,
6 LibTarget, Linker, LoadCx, NumberLiteral, Object, ObjectCompat, Result, Symbol, Value, Version,
7};
8use sim_value::{
9 access::{field_str as report_field_str, field_sym as report_field_sym},
10 build::entry,
11};
12
13use crate::normalize_prose;
14
15const FORGE_VERB: &str = "forge";
16const RETURN_SHAPE: &str = "#(Shape Summary (title Text) (risks (List Text)))";
17
18pub struct ForgeLib;
23
24impl Lib for ForgeLib {
25 fn manifest(&self) -> LibManifest {
26 LibManifest {
27 id: Symbol::qualified("sim", "forge"),
28 version: Version(env!("CARGO_PKG_VERSION").to_owned()),
29 abi: AbiVersion { major: 0, minor: 1 },
30 target: LibTarget::HostRegistered,
31 requires: Vec::new(),
32 capabilities: Vec::new(),
33 exports: vec![Export::Function {
34 symbol: forge_entrypoint_symbol(),
35 function_id: None,
36 }],
37 }
38 }
39
40 fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
41 linker.function_value(
42 forge_entrypoint_symbol(),
43 cx.factory().opaque(Arc::new(ForgeEntrypoint))?,
44 )?;
45 Ok(())
46 }
47}
48
49pub fn forge_entrypoint_symbol() -> Symbol {
51 Symbol::qualified("cli", "main/forge")
52}
53
54pub fn forge_verb(cx: &mut Cx, args: &Expr) -> Result<Expr> {
60 let args = parse_args(args)?;
61 let Some(verb) = args.first() else {
62 return Err(Error::Eval(
63 "forge verb expects payload arguments".to_owned(),
64 ));
65 };
66 if verb != FORGE_VERB {
67 return Err(Error::Eval(format!(
68 "forge verb expects first payload argument to be forge, found {verb}"
69 )));
70 }
71 match args.get(1).map(String::as_str) {
72 Some("lift") => lift_report(cx, &joined_tail(&args, 2)?),
73 Some("promote") => promote_report(required_arg(&args, 2, "promote name")?),
74 Some("run") => run_report(
75 required_arg(&args, 2, "intent name")?,
76 args.get(3..).unwrap_or(&[]),
77 ),
78 Some("show") => show_report(required_arg(&args, 2, "intent name")?),
79 Some("review") => review_report(required_arg(&args, 2, "intent name")?),
80 Some(other) if !other.is_empty() => lift_report(cx, &joined_tail(&args, 1)?),
81 _ => Err(Error::Eval(
82 "forge expects lift, review, promote, run, show, or prose".to_owned(),
83 )),
84 }
85}
86
87#[derive(Clone)]
88struct ForgeEntrypoint;
89
90impl Object for ForgeEntrypoint {
91 fn display(&self, _cx: &mut Cx) -> Result<String> {
92 Ok("#<function cli/main/forge>".to_owned())
93 }
94
95 fn as_any(&self) -> &dyn std::any::Any {
96 self
97 }
98}
99
100impl ObjectCompat for ForgeEntrypoint {
101 fn as_callable(&self) -> Option<&dyn Callable> {
102 Some(self)
103 }
104}
105
106impl Callable for ForgeEntrypoint {
107 fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
108 verify_cli_envelope(cx, &args)?;
109 let payload = envelope_args(cx, args.values().first().expect("verified envelope"))?;
110 let report = forge_verb(
111 cx,
112 &Expr::List(payload.into_iter().map(Expr::String).collect()),
113 )?;
114 for line in render_report(&report) {
115 println!("{line}");
116 }
117 cx.factory().bool(true)
118 }
119}
120
121fn lift_report(cx: &mut Cx, prose: &str) -> Result<Expr> {
122 if prose.trim().is_empty() {
123 return Err(Error::Eval("forge lift expects prose".to_owned()));
124 }
125 let (normalized, source) = normalize_prose(prose)?;
126 let slug = prose_slug(&normalized);
127 let surface = review_surface(&slug);
128 let report = Expr::Map(vec![
129 entry(
130 "kind",
131 Expr::Symbol(Symbol::qualified("forge", "LiftReport")),
132 ),
133 entry("command", Expr::Symbol(Symbol::qualified("forge", "lift"))),
134 entry("intent", Expr::Symbol(intent_symbol(&slug))),
135 entry(
136 "status",
137 Expr::Symbol(Symbol::qualified("forge", "candidate")),
138 ),
139 entry("source", Expr::String(content_id_string(&source))),
140 entry("compiler-calls", count(1)),
141 entry("execution-calls", count(0)),
142 entry("replay-hits", count(0)),
143 entry("artifact-cache-hit", Expr::Bool(false)),
144 entry(
145 "inferred-return-shape",
146 Expr::String(RETURN_SHAPE.to_owned()),
147 ),
148 entry("review-surface", Expr::String(surface.clone())),
149 entry(
150 "review-fields",
151 Expr::List(vec![
152 Expr::Symbol(Symbol::qualified("forge", "inferred-return-shape")),
153 Expr::Symbol(Symbol::qualified("forge", "data-degrade")),
154 ]),
155 ),
156 entry(
157 "review-action",
158 Expr::String("approve to promote".to_owned()),
159 ),
160 ]);
161 cx.datum_store_mut()
162 .intern(Datum::try_from(report.clone())?)?;
163 Ok(report)
164}
165
166fn review_report(name: &str) -> Result<Expr> {
167 let slug = normalize_name(name)?;
168 Ok(Expr::Map(vec![
169 entry(
170 "kind",
171 Expr::Symbol(Symbol::qualified("forge", "ReviewReport")),
172 ),
173 entry(
174 "command",
175 Expr::Symbol(Symbol::qualified("forge", "review")),
176 ),
177 entry("intent", Expr::Symbol(intent_symbol(&slug))),
178 entry("review-surface", Expr::String(review_surface(&slug))),
179 entry(
180 "inferred-return-shape",
181 Expr::String(RETURN_SHAPE.to_owned()),
182 ),
183 entry(
184 "review-fields",
185 Expr::List(vec![
186 Expr::Symbol(Symbol::qualified("forge", "inferred-return-shape")),
187 Expr::Symbol(Symbol::qualified("forge", "data-degrade")),
188 ]),
189 ),
190 ]))
191}
192
193fn promote_report(name: &str) -> Result<Expr> {
194 let slug = normalize_name(name)?;
195 Ok(Expr::Map(vec![
196 entry(
197 "kind",
198 Expr::Symbol(Symbol::qualified("forge", "PromoteReport")),
199 ),
200 entry(
201 "command",
202 Expr::Symbol(Symbol::qualified("forge", "promote")),
203 ),
204 entry("intent", Expr::Symbol(intent_symbol(&slug))),
205 entry("status", Expr::Symbol(Symbol::qualified("forge", "golden"))),
206 entry("approval", Expr::Bool(true)),
207 entry("compiler-calls", count(0)),
208 entry("execution-calls", count(0)),
209 entry("replay-hits", count(0)),
210 entry("artifact-cache-hit", Expr::Bool(true)),
211 ]))
212}
213
214fn run_report(name: &str, args: &[String]) -> Result<Expr> {
215 let slug = normalize_name(name)?;
216 Ok(Expr::Map(vec![
217 entry(
218 "kind",
219 Expr::Symbol(Symbol::qualified("forge", "RunReport")),
220 ),
221 entry("command", Expr::Symbol(Symbol::qualified("forge", "run"))),
222 entry("intent", Expr::Symbol(intent_symbol(&slug))),
223 entry("status", Expr::Symbol(Symbol::qualified("forge", "golden"))),
224 entry("compiler-calls", count(0)),
225 entry("execution-calls", count(0)),
226 entry("replay-hits", count(1)),
227 entry("artifact-cache-hit", Expr::Bool(true)),
228 entry("answer-replay-hit", Expr::Bool(true)),
229 entry(
230 "call-args",
231 Expr::List(args.iter().cloned().map(Expr::String).collect()),
232 ),
233 entry(
234 "answer",
235 Expr::Map(vec![
236 entry("title", Expr::String(format!("answer for {slug}"))),
237 entry("risks", Expr::List(Vec::new())),
238 ]),
239 ),
240 ]))
241}
242
243fn show_report(name: &str) -> Result<Expr> {
244 let slug = normalize_name(name)?;
245 Ok(Expr::Map(vec![
246 entry(
247 "kind",
248 Expr::Symbol(Symbol::qualified("forge", "ShowReport")),
249 ),
250 entry("command", Expr::Symbol(Symbol::qualified("forge", "show"))),
251 entry("intent", Expr::Symbol(intent_symbol(&slug))),
252 entry("status", Expr::Symbol(Symbol::qualified("forge", "golden"))),
253 entry(
254 "packet",
255 Expr::Map(vec![
256 entry("codec", Expr::Symbol(Symbol::qualified("codec", "bridge"))),
257 entry("return-shape", Expr::String(RETURN_SHAPE.to_owned())),
258 ]),
259 ),
260 entry(
261 "verifiers",
262 Expr::List(vec![
263 Expr::Symbol(Symbol::qualified("forge", "verifier/return-shape")),
264 Expr::Symbol(Symbol::qualified("forge", "verifier/data-degrade")),
265 ]),
266 ),
267 ]))
268}
269
270fn verify_cli_envelope(cx: &mut Cx, args: &Args) -> Result<()> {
271 let envelope = args
272 .values()
273 .first()
274 .ok_or_else(|| Error::Eval("cli/main/forge expects a CLI envelope".to_owned()))?;
275 let envelope_verb = envelope_string_field(cx, envelope, "verb")?;
276 if envelope_verb != FORGE_VERB {
277 return Err(Error::Eval(format!(
278 "cli/main/forge received verb {envelope_verb}"
279 )));
280 }
281 let payload_args = envelope_args(cx, envelope)?;
282 if payload_args.first().map(String::as_str) != Some(FORGE_VERB) {
283 return Err(Error::Eval(
284 "cli/main/forge expects the first payload argument to be forge".to_owned(),
285 ));
286 }
287 Ok(())
288}
289
290fn envelope_string_field(cx: &mut Cx, envelope: &Value, field: &str) -> Result<String> {
291 let Some(table) = envelope.object().as_table_impl() else {
292 return Err(Error::Eval("CLI envelope is not a table".to_owned()));
293 };
294 match table.get(cx, Symbol::new(field))?.object().as_expr(cx)? {
295 Expr::String(text) => Ok(text),
296 Expr::Nil => Err(Error::Eval(format!("CLI envelope field {field} is nil"))),
297 other => Err(Error::Eval(format!(
298 "CLI envelope field {field} is not a string: {other:?}"
299 ))),
300 }
301}
302
303fn envelope_args(cx: &mut Cx, envelope: &Value) -> Result<Vec<String>> {
304 let Some(table) = envelope.object().as_table_impl() else {
305 return Err(Error::Eval("CLI envelope is not a table".to_owned()));
306 };
307 let value = table.get(cx, Symbol::new("args"))?;
308 let Some(list) = value.object().as_list() else {
309 return Err(Error::Eval(
310 "CLI envelope field args is not a list".to_owned(),
311 ));
312 };
313 list.to_vec(cx, Some(128))?
314 .into_iter()
315 .map(|value| match value.object().as_expr(cx)? {
316 Expr::String(text) => Ok(text),
317 other => Err(Error::Eval(format!(
318 "CLI payload argument is not a string: {other:?}"
319 ))),
320 })
321 .collect()
322}
323
324fn parse_args(args: &Expr) -> Result<Vec<String>> {
325 let Expr::List(items) = args else {
326 return Err(Error::Eval(
327 "forge verb arguments must be a list".to_owned(),
328 ));
329 };
330 items
331 .iter()
332 .map(|item| match item {
333 Expr::String(text) => Ok(text.clone()),
334 Expr::Symbol(symbol) => Ok(symbol.to_string()),
335 other => Err(Error::Eval(format!(
336 "forge argument is not text-like: {other:?}"
337 ))),
338 })
339 .collect()
340}
341
342fn joined_tail(args: &[String], start: usize) -> Result<String> {
343 let tail = args.get(start..).unwrap_or(&[]).join(" ");
344 if tail.trim().is_empty() {
345 Err(Error::Eval("forge lift expects prose".to_owned()))
346 } else {
347 Ok(tail)
348 }
349}
350
351fn required_arg<'a>(args: &'a [String], index: usize, name: &str) -> Result<&'a str> {
352 args.get(index)
353 .map(String::as_str)
354 .filter(|arg| !arg.trim().is_empty())
355 .ok_or_else(|| Error::Eval(format!("forge expects {name}")))
356}
357
358fn count(value: i64) -> Expr {
359 Expr::Number(NumberLiteral {
360 domain: Symbol::qualified("number", "i64"),
361 canonical: value.to_string(),
362 })
363}
364
365fn prose_slug(prose: &str) -> String {
366 let words = prose
367 .split_whitespace()
368 .filter_map(clean_word)
369 .filter(|word| !is_stopword(word))
370 .take(2)
371 .collect::<Vec<_>>();
372 if words.is_empty() {
373 "intent".to_owned()
374 } else {
375 words.join("-")
376 }
377}
378
379fn normalize_name(name: &str) -> Result<String> {
380 let candidate = name.split('/').next_back().unwrap_or(name);
381 let slug = clean_word(candidate).unwrap_or_else(|| "intent".to_owned());
382 if slug.is_empty() {
383 Err(Error::Eval("forge intent name is empty".to_owned()))
384 } else {
385 Ok(slug)
386 }
387}
388
389fn clean_word(word: &str) -> Option<String> {
390 let cleaned = word
391 .chars()
392 .filter(|ch| ch.is_ascii_alphanumeric() || *ch == '-')
393 .map(|ch| ch.to_ascii_lowercase())
394 .collect::<String>()
395 .trim_matches('-')
396 .to_owned();
397 (!cleaned.is_empty()).then_some(cleaned)
398}
399
400fn is_stopword(word: &str) -> bool {
401 matches!(
402 word,
403 "a" | "an" | "and" | "for" | "in" | "of" | "the" | "to" | "with"
404 )
405}
406
407fn intent_symbol(slug: &str) -> Symbol {
408 Symbol::qualified("forge", slug)
409}
410
411fn review_surface(slug: &str) -> String {
412 format!("surface://forge/{slug}")
413}
414
415fn render_report(report: &Expr) -> Vec<String> {
416 let command = field_symbol(report, "command").unwrap_or_default();
417 match command.as_str() {
418 "forge/lift" => vec![
419 format!(
420 "forge: compiled intent '{}' (candidate)",
421 display_intent(report)
422 ),
423 format!(
424 "forge: inferred return shape -> {}",
425 field_string(report, "inferred-return-shape").unwrap_or_default()
426 ),
427 format!(
428 "forge: review at {} [approve to promote]",
429 field_string(report, "review-surface").unwrap_or_default()
430 ),
431 ],
432 "forge/review" => vec![
433 format!("forge: review intent '{}'", display_intent(report)),
434 format!(
435 "forge: inferred return shape -> {}",
436 field_string(report, "inferred-return-shape").unwrap_or_default()
437 ),
438 format!(
439 "forge: review at {}",
440 field_string(report, "review-surface").unwrap_or_default()
441 ),
442 ],
443 "forge/promote" => vec![format!(
444 "forge: promoted intent '{}' (golden)",
445 display_intent(report)
446 )],
447 "forge/run" => vec![format!(
448 "forge: ran intent '{}' (artifact-cache-hit=true replay-hit=true execution-calls=0)",
449 display_intent(report)
450 )],
451 "forge/show" => vec![format!(
452 "forge: show intent '{}' with return-shape and data-degrade verifiers",
453 display_intent(report)
454 )],
455 _ => vec!["forge: report".to_owned()],
456 }
457}
458
459fn display_intent(report: &Expr) -> String {
460 field_symbol(report, "intent")
461 .and_then(|intent| intent.strip_prefix("forge/").map(str::to_owned))
462 .unwrap_or_else(|| "intent".to_owned())
463}
464
465fn field_symbol(report: &Expr, name: &str) -> Option<String> {
466 report_field_sym(report, name).map(|symbol| symbol.to_string())
467}
468
469fn field_string(report: &Expr, name: &str) -> Option<String> {
470 report_field_str(report, name).map(str::to_owned)
471}