1use super::*;
4use serde_json::{json, Value};
5use std::collections::{BTreeMap, BTreeSet};
6
7fn alias_ref(text: &str) -> Option<(&str, &str)> {
8 let rest = text.strip_prefix('@')?;
9 let n = rest.find(['/', ':']).unwrap_or(rest.len());
10 Some((&rest[..n], &rest[n..]))
11}
12
13fn validate_reference(text: &str, aliases: &BTreeMap<String, String>) -> Result<(), String> {
15 if let Some((alias, suffix)) = alias_ref(text) {
16 if !aliases.contains_key(alias) {
17 return Err(format!("missing alias @{alias}"));
18 }
19 if !suffix.is_empty()
20 && !suffix.starts_with(':')
21 && suffix
22 .strip_prefix('/')
23 .and_then(|s| s.split_once('/'))
24 .filter(|(kind, _)| matches!(*kind, "output" | "face" | "edge"))
25 .and_then(|(_, index)| index.parse::<usize>().ok())
26 .is_none()
27 {
28 return Err(format!("invalid reference {text}"));
29 }
30 }
31 Ok(())
32}
33
34fn validate_references(
35 value: &Value,
36 aliases: &BTreeMap<String, String>,
37 path: &str,
38) -> Result<(), String> {
39 let mut refs = Vec::new();
40 strings(value, &mut refs);
41 for reference in refs {
42 validate_reference(&reference, aliases).map_err(|error| format!("{path}: {error}"))?;
43 }
44 Ok(())
45}
46
47fn strings(value: &Value, out: &mut Vec<String>) {
48 match value {
49 Value::String(s) => out.push(s.clone()),
50 Value::Array(a) => a.iter().for_each(|v| strings(v, out)),
51 Value::Object(o) => o.values().for_each(|v| strings(v, out)),
52 _ => (),
53 }
54}
55
56fn resolve(
57 value: &mut Value,
58 aliases: &BTreeMap<String, String>,
59 outputs: &BTreeMap<String, Vec<String>>,
60) -> Result<(), String> {
61 match value {
62 Value::String(s) => {
63 if let Some((alias, suffix)) = alias_ref(s) {
64 let id = aliases
65 .get(alias)
66 .ok_or_else(|| format!("missing alias @{alias}"))?;
67 *s = if let Some((kind, index)) =
68 suffix.strip_prefix('/').and_then(|s| s.split_once('/'))
69 {
70 if !matches!(kind, "output" | "face" | "edge") {
71 return Err(format!("invalid output kind in {s}"));
72 }
73 let index: usize = index
74 .parse()
75 .map_err(|_| format!("invalid output index in {s}"))?;
76 let key = if kind == "output" {
77 id.clone()
78 } else {
79 format!("{id}/{kind}")
80 };
81 outputs
82 .get(&key)
83 .and_then(|o| o.get(index))
84 .cloned()
85 .ok_or_else(|| format!("{kind} {index} of @{alias} was not generated"))?
86 } else if suffix.is_empty() || suffix.starts_with(':') {
87 format!("{id}{suffix}")
88 } else {
89 return Err(format!("invalid alias reference {s}"));
90 };
91 }
92 }
93 Value::Array(a) => {
94 for v in a {
95 resolve(v, aliases, outputs)?;
96 }
97 }
98 Value::Object(o) => {
99 for v in o.values_mut() {
100 resolve(v, aliases, outputs)?;
101 }
102 }
103 _ => (),
104 }
105 Ok(())
106}
107
108fn merge(base: &mut Value, patch: &Value) {
109 if let (Some(b), Some(p)) = (base.as_object_mut(), patch.as_object()) {
110 for (k, v) in p {
111 if let Some(old) = b.get_mut(k) {
112 merge(old, v);
113 } else {
114 b.insert(k.clone(), v.clone());
115 }
116 }
117 } else {
118 *base = patch.clone();
119 }
120}
121
122fn prepare(
123 catalogue: &Value,
124 items: &[Value],
125 history: &mut History,
126 aliases: &mut BTreeMap<String, String>,
127) -> Result<Vec<Value>, String> {
128 let mut ids: BTreeSet<String> = history
129 .features()
130 .iter()
131 .filter_map(|f| f["inputParams"]["id"].as_str().map(str::to_owned))
132 .collect();
133 ids.extend(aliases.values().cloned());
134 let mut features = Vec::new();
135 for (i, item) in items.iter().enumerate() {
136 let ty = item["type"]
137 .as_str()
138 .ok_or_else(|| format!("features[{i}].type is required"))?;
139 let schema = catalogue["features"].as_array().and_then(|fs| fs.iter().find(|f| f["type"] == ty || (!f["type"].as_str().unwrap_or("").contains('/') && f["shortName"] == ty))).cloned()
140 .ok_or_else(|| format!("features[{i}].type: unknown type {ty}"))?;
141 let mut params = json!({});
142 if let Some(properties) = schema["inputParamsSchema"].as_object() {
143 for (key, spec) in properties { params[key] = spec["default_value"].clone(); }
144 }
145 params["id"] = Value::Null;
146 let patch = item
147 .get("inputParams")
148 .or_else(|| item.get("params"))
149 .cloned()
150 .unwrap_or(json!({}));
151 let object = patch
152 .as_object()
153 .ok_or_else(|| format!("features[{i}].params must be an object"))?;
154 for key in object.keys() {
155 if params.get(key).is_none()
160 && !(super::components::is_acomp_feature_type(ty)
161 && (key == "occurrenceAttributes" || key == super::bom::OCCURRENCE_ATTRIBUTES))
162 && !(ty == "IMPORT3D" && key == "stepText")
163 {
164 return Err(format!("features[{i}].params.{key}: unknown field"));
165 }
166 }
167 merge(&mut params, &patch);
168 let id = item["id"]
169 .as_str()
170 .or_else(|| params["id"].as_str())
171 .map(str::to_owned)
172 .unwrap_or_else(|| loop {
173 let id = history.next_feature_id(schema["shortName"].as_str().unwrap_or(ty));
174 if !ids.contains(&id) {
175 break id;
176 }
177 });
178 if id.is_empty() || !ids.insert(id.clone()) {
179 return Err(format!("features[{i}].id: empty or duplicate id `{id}`"));
180 }
181 if matches!(ty, "ACOMP" | "ASSEMBLY COMPONENT") && !brep_kernel::is_component_reference(&id)
182 {
183 return Err(format!(
184 "features[{i}].id: component ids must be ACOMP<digits>"
185 ));
186 }
187 params["id"] = json!(id);
188 if super::components::is_acomp_feature_type(ty) {
189 if !object.contains_key("isFixed") {
190 params.as_object_mut().unwrap().remove("isFixed");
191 }
192 let record = super::bom::OCCURRENCE_ATTRIBUTES;
196 if let Some(legacy) = params.as_object_mut().and_then(|p| p.remove("occurrenceAttributes")) {
197 merge(&mut params[record], &legacy);
198 }
199 if let Some(attributes) = item.get("attributes") {
200 if !attributes.is_object() {
201 return Err(format!("features[{i}].attributes must be an object"));
202 }
203 merge(&mut params[record], attributes);
204 }
205 if let Some(key) = params[record].as_object().and_then(|object| {
206 object.keys().find(|key| super::part_mass::PART_ONLY_KEYS.contains(&key.as_str())).cloned()
207 }) {
208 return Err(format!(
209 "features[{i}].attributes.{key}: {key} is an attribute of the PART, not of one occurrence — put it in the part's partAttributes (or use `material`)"
210 ));
211 }
212 if let Some(name) = item.get("display_name") {
213 params[record]["Name"] = name.clone();
214 }
215 }
218 if let Some(alias) = item.get("alias") {
219 let alias = alias
220 .as_str()
221 .filter(|s| !s.is_empty() && !s.contains(['@', '/', ':']))
222 .ok_or_else(|| format!("features[{i}].alias: invalid alias"))?;
223 if aliases.insert(alias.into(), id.clone()).is_some() {
224 return Err(format!("features[{i}].alias: duplicate alias {alias}"));
225 }
226 }
227 brep_kernel::feature_pipeline::validate_literal_feature_params(schema["type"].as_str().unwrap_or_default(), ¶ms)?;
228 features.push(json!({"type":schema["type"], "inputParams":params, "persistentData":item.get("persistent_data").or_else(|| item.get("persistentData")).cloned().unwrap_or(json!({}))}));
229 }
230 Ok(features)
231}
232
233fn plan(
236 items: &[Value],
237 features: &[Value],
238 aliases: &BTreeMap<String, String>,
239) -> Result<(Vec<usize>, Vec<Vec<usize>>), String> {
240 let positions: BTreeMap<&str, usize> = features
241 .iter()
242 .enumerate()
243 .map(|(i, f)| (f["inputParams"]["id"].as_str().unwrap(), i))
244 .collect();
245 let mut deps = vec![Vec::new(); features.len()];
246 for (i, feature) in features.iter().enumerate() {
247 let mut refs = Vec::new();
248 strings(&feature["inputParams"], &mut refs);
249 strings(&feature["persistentData"], &mut refs);
250 if let Some(explicit) = items[i].get("depends_on") {
251 let a = explicit
252 .as_array()
253 .ok_or_else(|| format!("features[{i}].depends_on must be an array"))?;
254 for r in a {
255 refs.push(format!(
256 "@{}",
257 r.as_str().ok_or("depends_on entries must be aliases")?
258 ));
259 }
260 }
261 for r in refs {
262 validate_reference(&r, aliases).map_err(|error| format!("features[{i}]: {error}"))?;
263 if let Some((a, _)) = alias_ref(&r) {
264 let id = &aliases[a];
265 if let Some(&p) = positions.get(id.as_str()) {
266 deps[i].push(p);
267 }
268 }
269 }
270 deps[i].sort_unstable();
271 deps[i].dedup();
272 }
273 let mut order = Vec::new();
274 while order.len() < features.len() {
275 let Some(i) = (0..features.len())
276 .find(|i| !order.contains(i) && deps[*i].iter().all(|d| order.contains(d)))
277 else {
278 return Err(format!(
279 "dependency cycle among feature positions {:?}",
280 (0..features.len())
281 .filter(|i| !order.contains(i))
282 .collect::<Vec<_>>()
283 ));
284 };
285 order.push(i);
286 }
287 Ok((order, deps))
288}
289
290#[derive(Debug, Clone)]
292pub(crate) struct StagedFeature {
293 id: String,
294 before: History,
295 metadata: crate::metadata::MetadataStore,
296 library: brep_kernel::PartsLibraryMap,
297 staged: History,
298 edit_serial: u64,
300}
301
302fn failed(report: &Value) -> bool {
303 report.get("error").is_some()
304 || [
305 "featureErrors",
306 "displayErrors",
307 "unresolved",
308 "featureFulfilment",
309 ]
310 .iter()
311 .any(|key| match &report[*key] {
312 Value::Object(o) => !o.is_empty(),
313 Value::Array(a) => !a.is_empty(),
314 _ => false,
315 })
316}
317
318fn refusal_category(refusal: &Value) -> &'static str {
319 match refusal["class"].as_str() {
320 Some("degenerate_arrangement" | "non_integral_genus" | "invalid_result_topology") => {
321 "invalid_topology"
322 }
323 Some("tangent_node_singularity" | "conservative_empty_overlap") => "contact_related",
324 Some("non_convergence") => "numerical_nonconvergence",
325 Some("invalid_input") => "invalid_input",
326 Some("unsupported_geometry") => "unsupported_geometry",
327 _ => "geometry_execution_failure",
328 }
329}
330
331impl EngineState {
332 pub fn geometry_diagnostics(&self, id: &str) -> Option<Value> {
334 self.geometry_diagnostics.get(id).cloned()
335 }
336
337 pub fn stage_feature_begin(&mut self, item: &Value) -> Result<String, String> {
349 if self.staged_feature.is_some() {
350 return Err("document_busy: a staged feature is still evaluating".into());
351 }
352 if self.run_pending() || self.queries_pending() || self.mesh_imports_pending() || self.topology_pending() {
353 return Err("wait for pending engine work before staging a feature".into());
354 }
355 if let Some(reason) = self.history.locked() {
356 return Err(format!("document is locked: {reason}"));
357 }
358 let before = self.history.clone();
359 let metadata = self.metadata.clone();
360 let library = brep_kernel::parts_library_map();
361 let mut staged = before.clone();
362 let mut aliases = BTreeMap::new();
363 if item.get("material").is_some() {
364 return Err("material is a PART attribute: set it with bom_set_part_attribute, or in a feature_add_many part's document".into());
365 }
366 let features = prepare(&self.feature_catalogue(), std::slice::from_ref(item), &mut staged, &mut aliases)?;
367 let feature = features.into_iter().next().ok_or("nothing to stage")?;
368 let id = feature["inputParams"]["id"].as_str().ok_or("the staged feature has no id")?.to_string();
369 let mut validation: Value = serde_json::from_str(&before.request_json()).map_err(|e| e.to_string())?;
370 validation["features"].as_array_mut().ok_or("document features missing")?.push(feature.clone());
371 serde_json::from_value::<HistoryRequest>(validation).map_err(|e| format!("stage validation: {e}"))?;
372 self.add_features(&[feature]);
373 let edit_serial = self.history.edit_serial();
377 self.history.set_busy(Some("an agent's change to this document is still evaluating".into()));
380 self.staged_feature = Some(StagedFeature { id: id.clone(), before, metadata, library, staged, edit_serial });
381 Ok(id)
382 }
383
384 pub fn stage_feature_pending(&self) -> bool {
386 self.staged_feature.is_some()
387 }
388
389 pub fn stage_feature_poll(&mut self) -> Option<Result<Value, String>> {
393 if self.staged_feature.is_none() || self.run_pending() {
394 return None;
395 }
396 let st = self.staged_feature.take()?;
397 self.history.set_busy(None);
398 if self.history.busy_refused() > self.busy_refused_seen {
401 self.push_notice("Not applied: an agent's change to this document was still evaluating. Try again.");
402 }
403 let report: Value = serde_json::from_str(&self.history_report_json()).unwrap_or(Value::Null);
404 let id = st.id.clone();
405 let names: Vec<String> = serde_json::from_value(report["featureOutputs"][&id].clone()).unwrap_or_default();
406 let errors: Vec<String> = report["featureErrors"]
407 .as_array()
408 .into_iter()
409 .flatten()
410 .filter_map(Value::as_str)
411 .filter(|e| e.starts_with(&format!("{id}:")))
412 .map(|e| e.chars().take(240).collect())
413 .collect();
414 let evaluated = report["featureTimings"].get(&id).is_some();
415 let failed = !errors.is_empty() || report["featureFulfilment"].get(&id).is_some() || report.get("error").is_some() || !evaluated;
416 let interleaved = self.history.edit_serial() != st.edit_serial;
418 if failed && interleaved {
419 if let Some(index) = self.history.index_of(&id) {
422 self.history.remove_feature(index);
423 }
424 self.rerun_history();
425 let why = if errors.is_empty() { "the feature did not evaluate".to_string() } else { errors.join("; ") };
426 self.busy_refused_seen = self.history.busy_refused();
427 return Some(Ok(json!({ "id": id, "committed": false, "status": "failed", "generatedSolids": [], "error": why, "rolledBack": true, "interleavedEdit": true })));
428 }
429 if failed {
430 self.history = st.before;
432 self.metadata = st.metadata;
433 brep_kernel::install_parts_library(&Default::default());
434 brep_kernel::install_parts_library(&st.library);
435 self.rerun_history();
436 let why = if errors.is_empty() { "the feature did not evaluate".to_string() } else { errors.join("; ") };
437 self.busy_refused_seen = self.history.busy_refused();
438 return Some(Ok(json!({ "id": id, "committed": false, "status": "failed", "generatedSolids": [], "error": why, "rolledBack": true })));
439 }
440 if interleaved {
441 self.history.retain_feature_allocations(&st.staged);
444 self.busy_refused_seen = self.history.busy_refused();
445 return Some(Ok(json!({ "id": id, "committed": true, "status": "evaluated", "generatedSolids": names, "rolledBack": false, "interleavedEdit": true })));
446 }
447 let final_document = self.history.request_json();
449 self.history = st.before;
450 if let Err(e) = self.history.adopt_document_checkpointed(&final_document) {
451 return Some(Err(e));
452 }
453 self.history.retain_feature_allocations(&st.staged);
454 self.history.set_rollback(self.history.len().saturating_sub(1));
455 self.busy_refused_seen = self.history.busy_refused();
456 Some(Ok(json!({ "id": id, "committed": true, "status": "evaluated", "generatedSolids": names, "rolledBack": false })))
457 }
458
459 fn finish_batch_run(&mut self) {
460 while self.run_pending() {
461 self.pump();
462 #[cfg(not(target_arch = "wasm32"))]
463 if self.run_pending() {
464 std::thread::sleep(std::time::Duration::from_millis(1));
465 }
466 }
467 }
468
469 pub fn author_batch(&mut self, args: &Value) -> Result<Value, String> {
473 if cfg!(target_arch = "wasm32") {
474 return Err("structured native authoring requires a native MCP host".into());
475 }
476 if self.run_pending()
477 || self.queries_pending()
478 || self.mesh_imports_pending()
479 || self.topology_pending()
480 {
481 return Err("wait for pending engine work before authoring a batch".into());
482 }
483 if let Some(reason) = self.history.locked() {
484 return Err(format!("document is locked: {reason}"));
485 }
486 let items = args["features"]
487 .as_array()
488 .ok_or("features must be an array")?;
489 let rollback = args["rollback_on_error"].as_bool().unwrap_or(false);
490 let before = self.history.clone();
491 let metadata = self.metadata.clone();
492 let library = brep_kernel::parts_library_map();
493 let mut staged = before.clone();
494 let mut document: Value =
495 serde_json::from_str(&staged.request_json()).map_err(|e| e.to_string())?;
496 let mut aliases = BTreeMap::new();
497 let mut part_aliases = BTreeMap::new();
498 let mut part_work = Vec::new();
499 let mut constraint_ids = Vec::new();
500 if let Some(parts) = args.get("parts") {
501 for (i, part) in parts
502 .as_array()
503 .ok_or("parts must be an array")?
504 .iter()
505 .enumerate()
506 {
507 let alias = part["alias"]
508 .as_str()
509 .filter(|s| !s.is_empty() && !s.contains(['@', '/', ':']))
510 .ok_or_else(|| format!("parts[{i}].alias is required"))?;
511 if aliases.insert(alias.into(), alias.into()).is_some()
512 || document["partsLibrary"].get(alias).is_some()
513 {
514 return Err(format!("parts[{i}]: duplicate part {alias}"));
515 }
516 let mut doc = part["document"].clone();
517 let native = doc["features"]
518 .as_array()
519 .ok_or_else(|| format!("parts[{i}].document.features must be an array"))?
520 .clone();
521 let mut allocation_base = doc.clone();
522 allocation_base["features"] = json!([]);
523 let mut part_history = History::from_request_json(&allocation_base.to_string())?;
524 let mut local_aliases = BTreeMap::new();
525 let mut scope = EngineState::new();
526 scope.inherit_plugins(self);
527 scope.history = part_history.clone();
528 let mut prepared = prepare(&scope.feature_catalogue(), &native, &mut part_history, &mut local_aliases)
529 .map_err(|e| format!("parts[{i}].document.{e}"))?;
530 let _ = plan(&native, &prepared, &local_aliases)
531 .map_err(|e| format!("parts[{i}].document.{e}"))?;
532 for (index, f) in prepared.iter_mut().enumerate() {
533 for key in ["alias", "depends_on"] {
534 if let Some(value) = native[index].get(key) {
535 f[key] = value.clone();
536 }
537 }
538 }
539 part_aliases.insert(alias.to_owned(), local_aliases);
540 doc["features"] = json!(prepared);
543 doc["featureCounter"] = serde_json::from_str::<Value>(&part_history.request_json())
544 .unwrap_or_default()["featureCounter"]
545 .clone();
546 part_work.push((alias.to_owned(), doc.clone()));
547 document["partsLibrary"][alias] = json!({"sourceKey":alias,"sourceSignature":"batch", "document":doc,"snapshot":""});
548 }
549 }
550 let mut features = prepare(&self.feature_catalogue(), items, &mut staged, &mut aliases)?;
551 let mut materials: BTreeMap<String, Value> = BTreeMap::new();
557 for (i, item) in items.iter().enumerate() {
558 let Some(material) = item.get("material") else { continue };
559 let part = features[i]["inputParams"]["partName"].as_str().unwrap_or_default();
560 let part = part.strip_prefix('@').unwrap_or(part).to_string();
561 if !part_work.iter().any(|(alias, _)| *alias == part) {
562 return Err(format!(
563 "features[{i}].material: Material is an attribute of the PART '{part}', not of this occurrence — set it with bom_set_part_attribute (only a part defined in this batch's `parts` takes it here)"
564 ));
565 }
566 if let Some(previous) = materials.insert(part.clone(), material.clone()) {
567 if previous != *material {
568 return Err(format!(
569 "features[{i}].material: part '{part}' is given two materials ({previous} and {material}); a material belongs to the part, so its components must agree"
570 ));
571 }
572 }
573 }
574 for (part, material) in materials {
575 for (alias, doc) in part_work.iter_mut() {
576 if *alias == part {
577 doc[super::bom::PART_ATTRIBUTES]["Material"] = material.clone();
578 document["partsLibrary"][alias.as_str()]["document"] = doc.clone();
579 }
580 }
581 }
582 let (order, deps) = plan(items, &features, &aliases)?;
583 document["featureCounter"] = serde_json::from_str::<Value>(&staged.request_json())
584 .unwrap_or_default()["featureCounter"]
585 .clone();
586 if let Some(expressions) = args.get("expressions") {
587 if !expressions.is_string() {
588 return Err("expressions must be a script string".into());
589 }
590 document["expressions"] = expressions.clone();
591 }
592 if let Some(constraints) = args.get("constraints") {
594 let catalogue = brep_kernel::constraint_schema_catalogue();
595 let mut counter = document["assembly"]["idCounter"].as_u64().unwrap_or(0);
596 for (i, constraint) in constraints
597 .as_array()
598 .ok_or("constraints must be an array")?
599 .iter()
600 .enumerate()
601 {
602 let ty = constraint["type"]
603 .as_str()
604 .ok_or("constraint type is required")?;
605 if ty == "fixed" {
606 return Err(format!("constraints[{i}].type: {}", brep_kernel::FIXED_IS_NOT_A_CONSTRAINT));
607 }
608 let entry = catalogue
609 .as_array()
610 .and_then(|a| a.iter().find(|e| e["type"].as_str() == Some(ty)))
611 .ok_or_else(|| format!("constraints[{i}].type: unknown constraint {ty}"))?;
612 counter += 1;
613 let id = format!("{}{}", entry["shortName"].as_str().unwrap_or("C"), counter);
614 let mut params = json!({});
615 if let Some(schema) = entry["inputParamsSchema"].as_object() {
616 for (k, v) in schema {
617 params[k] = v["default_value"].clone();
618 }
619 }
620 merge(
621 &mut params,
622 constraint
623 .get("params")
624 .or_else(|| constraint.get("inputParams"))
625 .unwrap_or(&json!({})),
626 );
627 for key in params
628 .as_object()
629 .ok_or_else(|| format!("constraints[{i}].params must be an object"))?
630 .keys()
631 {
632 if entry["inputParamsSchema"].get(key).is_none() {
633 return Err(format!("constraints[{i}].params.{key}: unknown field"));
634 }
635 }
636 constraint_ids.push(id.clone());
637 params["id"] = json!(id);
638 validate_references(¶ms, &aliases, &format!("constraints[{i}].params"))?;
639 if let Some(a) = constraint["alias"].as_str() {
640 if aliases.insert(a.into(), id).is_some() {
641 return Err(format!("constraints[{i}].alias is duplicate"));
642 }
643 }
644 if !document["assembly"]["constraints"].is_array() {
645 document["assembly"]["constraints"] = json!([]);
646 }
647 let mut persistent = constraint
648 .get("persistent_data")
649 .or_else(|| constraint.get("persistentData"))
650 .cloned()
651 .unwrap_or(json!({}));
652 validate_references(
653 &persistent,
654 &aliases,
655 &format!("constraints[{i}].persistentData"),
656 )?;
657 let explicit = constraint
658 .get("params")
659 .or_else(|| constraint.get("inputParams"));
660 for (field, flag) in [
663 ("distance", "initializedDistance"),
664 ("angle", "initializedAngle"),
665 ] {
666 if explicit.is_some_and(|p| p.get(field).is_some()) {
667 persistent
668 .as_object_mut()
669 .ok_or_else(|| {
670 format!("constraints[{i}].persistent_data must be an object")
671 })?
672 .entry(flag)
673 .or_insert(json!(true));
674 }
675 }
676 document["assembly"]["constraints"].as_array_mut().unwrap().push(json!({"type":ty,"inputParams":params,"persistentData":persistent,"enabled":constraint.get("enabled").cloned().unwrap_or(json!(true))}));
677 }
678 document["assembly"]["idCounter"] = json!(counter);
679 }
680 if let Some(patch) = args.get("metadata") {
681 let map = patch.as_object().ok_or("metadata must be an object")?;
682 for key in map.keys() {
683 if let Some((a, _)) = alias_ref(key) {
684 if !aliases.contains_key(a) {
685 return Err(format!("metadata.{key}: missing alias @{a}"));
686 }
687 }
688 }
689 }
690 let mut validation = document.clone();
692 validation["features"]
693 .as_array_mut()
694 .ok_or("document features missing")?
695 .extend(features.clone());
696 serde_json::from_value::<HistoryRequest>(validation)
697 .map_err(|e| format!("batch validation: {e}"))?;
698 self.geometry_diagnostics.clear();
699 let run = (|| -> Result<Value, String> {
700 let mut part_results = Vec::new();
704 for (position, (alias, source)) in part_work.iter().enumerate() {
705 let mut base = source.clone();
706 let part_features = base["features"].take();
707 base["features"] = json!([]);
708 base.as_object_mut().unwrap().remove("metadata");
709 let mut part = EngineState::new();
710 part.inherit_plugins(self);
711 part.set_history_json(&base.to_string())?;
712 let mut part_args = json!({"features":part_features,"rollback_on_error":true});
713 if let Some(metadata) = source.get("metadata") {
714 part_args["metadata"] = metadata.clone();
715 }
716 let mut evaluated = part.author_batch(&part_args)?;
717 for (id, details) in &part.geometry_diagnostics {
718 self.geometry_diagnostics
719 .insert(format!("part:{alias}:{id}"), details.clone());
720 }
721 for item in evaluated["items"].as_array_mut().into_iter().flatten() {
722 if let Some(id) = item["diagnostic"]["detailId"].as_str().map(str::to_owned) {
723 item["diagnostic"]["detailId"] = json!(format!("part:{alias}:{id}"));
724 }
725 }
726 part_results.push(json!({"position":position,"alias":alias,"id":alias,"status":if evaluated["success"] == true {"evaluated"} else {"failed"},"committed":false,"items":evaluated["items"],"outputs":evaluated["outputs"]}));
727 if evaluated["success"] != true {
728 for (i, (remaining, _)) in part_work.iter().enumerate().skip(position + 1) {
729 part_results.push(json!({"position":i,"alias":remaining,"id":remaining,"status":"not_evaluated","committed":false}));
730 }
731 let pending: Vec<Value> = items.iter().enumerate().map(|(i,item)| json!({"position":i,"alias":item["alias"],"id":features[i]["inputParams"]["id"],"status":"not_evaluated","committed":false,"dependencyFailure":{"part":alias}})).collect();
732 return Ok(
733 json!({"success":false,"requiresRestore":true,"items":pending,"partResults":part_results,"constraintResults":[],"aliases":aliases,"partAliases":part_aliases,"diagnosticsTool":"geometry_diagnostics"}),
734 );
735 }
736 document["partsLibrary"][alias]["document"] =
737 serde_json::from_str::<Value>(&part.history_request_json())
738 .map_err(|e| e.to_string())?;
739 }
740 let constraints = document.get("assembly").cloned();
742 document.as_object_mut().unwrap().remove("assembly");
743 self.edit_document_json(&document.to_string())?;
744 self.finish_batch_run();
745 let prepared_library =
746 serde_json::from_value(document.get("partsLibrary").cloned().unwrap_or(json!({})))
747 .map_err(|e| format!("partsLibrary: {e}"))?;
748 let mut outputs: BTreeMap<String, Vec<String>> = BTreeMap::new();
749 let mut results = vec![Value::Null; items.len()];
750 let mut failed_items = BTreeSet::new();
751 for i in order {
752 let id = features[i]["inputParams"]["id"]
753 .as_str()
754 .unwrap()
755 .to_string();
756 let blocked: Vec<usize> = deps[i]
757 .iter()
758 .filter(|d| failed_items.contains(*d))
759 .copied()
760 .collect();
761 let mut result = json!({"position":i,"alias":items[i]["alias"],"id":id,"committed":false,"dependencies":deps[i]});
762 if !blocked.is_empty() {
763 failed_items.insert(i);
764 result["status"] = json!("not_evaluated");
765 result["dependencyFailures"] = json!(blocked);
766 results[i] = result;
767 continue;
768 }
769 if let Err(error) = resolve(&mut features[i], &aliases, &outputs) {
770 failed_items.insert(i);
771 result["status"] = json!("failed");
772 result["error"] = json!({"category":"reference_resolution","message":error,"path":format!("features[{i}]")});
773 results[i] = result;
774 continue;
775 }
776 brep_kernel::install_parts_library(&prepared_library);
777 self.history.set_parts_library(
778 serde_json::to_value(brep_kernel::parts_library_map())
779 .map_err(|e| e.to_string())?,
780 );
781 self.add_features(&[features[i].clone()]);
782 self.finish_batch_run();
783 let report: Value =
784 serde_json::from_str(&self.history_report_json()).map_err(|e| e.to_string())?;
785 let names: Vec<String> =
786 serde_json::from_value(report["featureOutputs"][&id].clone())
787 .unwrap_or_default();
788 outputs.insert(id.clone(), names.clone());
789 let errors: Vec<Value> = report["featureErrors"]
790 .as_array()
791 .into_iter()
792 .flatten()
793 .filter(|e| e.as_str().is_some_and(|e| e.starts_with(&format!("{id}:"))))
794 .cloned()
795 .collect();
796 let has_error = !errors.is_empty()
797 || report["featureFulfilment"].get(&id).is_some()
798 || report.get("error").is_some();
799 if has_error {
800 failed_items.insert(i);
801 }
802 result["status"] = json!(if has_error {
803 "failed"
804 } else if report["featureTimings"].get(&id).is_some() {
805 "evaluated"
806 } else {
807 "not_evaluated"
808 });
809 if result["status"] == "not_evaluated" {
810 failed_items.insert(i);
811 }
812 result["generatedSolids"] = json!(names);
813 self.geometry_diagnostics.insert(id.clone(), json!({"errors":errors,"refusal":report["featureRefusals"][&id],"unresolved":report["unresolved"],"fulfilment":report["featureFulfilment"][&id],"approximations":report["featureApproximations"][&id],"operation":features[i]["type"],"operands":features[i]["inputParams"],"dependencies":deps[i]}));
814 result["error"] = json!(errors
815 .iter()
816 .filter_map(Value::as_str)
817 .map(|s| s.chars().take(240).collect::<String>())
818 .collect::<Vec<_>>());
819 let mut references = Vec::new();
820 for name in &names {
821 if let Some(solid) = self.scene.solid(name) {
822 references.push(json!({"solid":name,"faces":solid.faces.iter().map(|f| &f.name).collect::<Vec<_>>(),"edges":solid.edges.iter().map(|e| &e.name).collect::<Vec<_>>()}));
823 }
824 }
825 outputs.insert(
826 format!("{id}/face"),
827 references
828 .iter()
829 .flat_map(|r| r["faces"].as_array().into_iter().flatten())
830 .filter_map(Value::as_str)
831 .map(str::to_owned)
832 .collect(),
833 );
834 outputs.insert(
835 format!("{id}/edge"),
836 references
837 .iter()
838 .flat_map(|r| r["edges"].as_array().into_iter().flatten())
839 .filter_map(Value::as_str)
840 .map(str::to_owned)
841 .collect(),
842 );
843 result["generatedReferences"] = json!(references);
844 let schema =
845 self.feature_schema(features[i]["type"].as_str().unwrap_or(""))
846 .unwrap_or_default();
847 let operands: serde_json::Map<String, Value> = features[i]["inputParams"]
848 .as_object()
849 .into_iter()
850 .flat_map(|o| o.iter())
851 .filter(|(k, _)| {
852 matches!(
853 schema["inputParamsSchema"][*k]["type"].as_str(),
854 Some("reference_selection" | "boolean_operation")
855 ) || k.as_str() == "partName"
856 })
857 .map(|(k, v)| (k.clone(), v.clone()))
858 .collect();
859 result["diagnostic"] = json!({"operation":features[i]["type"],"owningComponent": if super::components::is_acomp_feature_type(features[i]["type"].as_str().unwrap_or("")) { Some(&id) } else { None }, "operands":operands,"affectedTopology":report["featureRefusals"][&id]["faces"],"category":if has_error { Some(refusal_category(&report["featureRefusals"][&id])) } else { None },"kernelClass":report["featureRefusals"][&id]["class"],"detailId":id});
860 result["committed"] = json!(true);
861 results[i] = result;
862 }
863 let mut constraint_errors = BTreeMap::new();
864 if let Some(mut constraints) = constraints {
865 if let Some(entries) = constraints["constraints"].as_array_mut() {
866 entries.retain_mut(|entry| {
867 let id = entry["inputParams"]["id"].as_str().unwrap_or("").to_owned();
868 if let Err(error) = resolve(entry, &aliases, &outputs) {
869 constraint_errors.insert(id, error);
870 false
871 } else {
872 true
873 }
874 });
875 }
876 let mut final_doc: Value = serde_json::from_str(&self.history.request_json())
877 .map_err(|e| e.to_string())?;
878 final_doc["assembly"] = constraints;
879 self.edit_document_json(&final_doc.to_string())?;
880 self.finish_batch_run();
881 }
882 if let Some(patch) = args.get("metadata").and_then(Value::as_object) {
883 let mut merged = self.metadata.to_json();
884 for (key, record) in patch {
885 let mut name = json!(key);
886 resolve(&mut name, &aliases, &outputs)?;
887 merge(&mut merged[name.as_str().unwrap()], record);
888 }
889 self.metadata.load_json(Some(&merged));
890 self.sync_colors_from_metadata();
891 }
892 let final_report: Value =
893 serde_json::from_str(&self.history_report_json()).map_err(|e| e.to_string())?;
894 let statuses = self.assembly_statuses_value();
895 let constraint_failure = statuses.as_array().is_some_and(|a| {
896 a.iter()
897 .any(|s| s["enabled"] != false && s["satisfied"] == false)
898 });
899 let success = failed_items.is_empty()
900 && !failed(&final_report)
901 && !constraint_failure
902 && constraint_errors.is_empty();
903 let constraint_results: Vec<Value> = constraint_ids.iter().enumerate().map(|(i,id)| {
904 let row = statuses.as_array().and_then(|a| a.iter().find(|r| r["id"].as_str() == Some(id)));
905 let state = if constraint_errors.contains_key(id) || row.is_none() || row.is_some_and(|r| r["enabled"] == false) { "not_evaluated" } else if row.is_some_and(|r| r["satisfied"] == false) { "failed" } else { "evaluated" };
906 json!({"position":i,"alias":args["constraints"][i]["alias"],"id":id,"status":state,"committed":!constraint_errors.contains_key(id),"error":constraint_errors.get(id),"diagnostics":row,"dependencyFailures":if constraint_errors.contains_key(id) { failed_items.iter().copied().collect::<Vec<_>>() } else { vec![] }})
907 }).collect();
908 let mut retained_library = brep_kernel::parts_library_map();
911 for (alias, _) in &part_work {
912 if let Some(entry) = prepared_library.get(alias) {
913 retained_library
914 .entry(alias.clone())
915 .or_insert_with(|| entry.clone());
916 }
917 }
918 brep_kernel::install_parts_library(&retained_library);
919 self.history.set_parts_library(
920 serde_json::to_value(&retained_library).map_err(|e| e.to_string())?,
921 );
922 for result in &mut part_results {
923 result["committed"] = json!(true);
924 }
925 self.geometry_diagnostics
926 .insert("__report__".into(), final_report);
927 Ok(
928 json!({"success":success,"items":results,"partResults":part_results,"constraintResults":constraint_results,"aliases":aliases,"partAliases":part_aliases,"outputs":outputs,"diagnosticsTool":"geometry_diagnostics","constraintStatuses":statuses,"dof":self.assembly_dof_value()}),
929 )
930 })();
931 let restore = run
932 .as_ref()
933 .map(|r| r["requiresRestore"] == true || (rollback && r["success"] != true))
934 .unwrap_or(true);
935 if restore {
936 self.history = before.clone();
937 self.metadata = metadata.clone();
938 brep_kernel::install_parts_library(&Default::default());
939 brep_kernel::install_parts_library(&library);
940 self.rerun_history();
941 self.finish_batch_run();
942 self.history = before;
945 self.metadata = metadata;
946 brep_kernel::install_parts_library(&Default::default());
947 brep_kernel::install_parts_library(&library);
948 } else {
949 let final_document = self.history.request_json();
951 self.history = before;
952 self.history.adopt_document_checkpointed(&final_document)?;
953 self.history.retain_feature_allocations(&staged);
954 self.history
955 .set_rollback(self.history.len().saturating_sub(1));
956 }
957 let mut result = run?;
958 result.as_object_mut().unwrap().remove("requiresRestore");
959 result["rolledBack"] = json!(restore);
960 result["transaction"] = json!({"validationAtomic":true,"geometryAtomic":rollback,"idCountersRolledBack":restore,"runtimeGenerationRolledBack":false});
961 if restore {
962 fn uncommit(value: &mut Value) {
963 match value {
964 Value::Object(object) => {
965 if object.contains_key("committed") {
966 object.insert("committed".into(), json!(false));
967 }
968 for value in object.values_mut() {
969 uncommit(value);
970 }
971 }
972 Value::Array(array) => {
973 for value in array {
974 uncommit(value);
975 }
976 }
977 _ => (),
978 }
979 }
980 uncommit(&mut result);
981 }
982 Ok(result)
983 }
984}
985
986pub(super) fn contains_expression(value: &Value) -> bool {
988 match value {
989 Value::String(_) => true,
990 Value::Array(a) => a.iter().any(contains_expression),
991 Value::Object(o) => o.values().any(contains_expression),
992 _ => false,
993 }
994}
995
996
997
998
999