Skip to main content

brep_render/engine_state/
plugins.rs

1//! Trusted package installations and document-scoped, asynchronous transactions.
2use super::EngineState;
3use crate::history::History;
4use brep_plugins::{PackageBundle, PluginPin, Runtime};
5use serde_json::{json, Value};
6use std::collections::VecDeque;
7
8#[derive(Default)]
9pub(super) struct Plugins {
10    packages: Vec<(PackageBundle, bool)>,
11    runtime: Option<Runtime>,
12    revision: u64,
13    next_action: u64,
14    pending: Option<PendingAction>,
15    results: VecDeque<Result<Value, String>>,
16    status: Value,
17    install_pending: Option<PendingInstall>,
18    install_status: Value,
19    deferred_run: bool,
20}
21
22struct PendingInstall {
23    id: u64,
24    revision: u64,
25    packages: Vec<(PackageBundle, bool)>,
26}
27
28fn parse_installations(packages: Value) -> Result<Vec<(PackageBundle, bool)>, String> {
29    let rows = packages
30        .as_array()
31        .ok_or("installed plugins must be an array")?;
32    let mut installed = Vec::new();
33    let mut seen = std::collections::BTreeSet::new();
34    for row in rows {
35        let bundle: PackageBundle = serde_json::from_value(row["bundle"].clone())
36            .map_err(|e| format!("plugin bundle: {e}"))?;
37        let pin = bundle.pin()?;
38        if !seen.insert((pin.id.clone(), pin.digest)) {
39            return Err(format!("duplicate installed package {}", pin.id));
40        }
41        let enabled = row["enabled"]
42            .as_bool()
43            .ok_or("plugin enabled must be boolean")?;
44        installed.push((bundle, enabled));
45    }
46    Ok(installed)
47}
48
49struct PendingAction {
50    id: u64,
51    revision: u64,
52    registry_revision: u64,
53    selection: String,
54    generation: u64,
55    pins: Vec<PluginPin>,
56    staged: Option<History>,
57    notifications: Vec<String>,
58    annotation_id: Option<String>,
59    allowed_annotation_errors: Vec<AnnotationFailure>,
60}
61
62impl Plugins {
63    fn bundles(&self) -> Vec<PackageBundle> {
64        self.packages
65            .iter()
66            .filter(|(_, enabled)| *enabled)
67            .map(|(p, _)| p.clone())
68            .collect()
69    }
70    fn rebuild(&mut self, packages: Vec<(PackageBundle, bool)>) -> Result<(), String> {
71        let enabled = packages
72            .iter()
73            .filter(|(_, enabled)| *enabled)
74            .map(|(p, _)| p.clone())
75            .collect();
76        let runtime = Runtime::new(enabled)?;
77        self.packages = packages;
78        self.runtime = Some(runtime);
79        self.revision = self.revision.wrapping_add(1);
80        Ok(())
81    }
82}
83
84impl EngineState {
85    /// Restore persisted installations in one atomic registry revision.
86    pub fn restore_plugins(&mut self, packages: Value) -> Result<(), String> {
87        self.plugins.rebuild(parse_installations(packages)?)?;
88        self.plugins_changed();
89        Ok(())
90    }
91
92    /// Submit a complete installation set to the runner for atomic validation.
93    /// Does not enable code or change an existing registry before the reply succeeds.
94    pub fn submit_plugin_installations(&mut self, installations: Value) -> Result<u64, String> {
95        if self.plugins.install_pending.is_some() {
96            return Err("plugin installation already pending".into());
97        }
98        let packages = parse_installations(installations)?;
99        self.plugins.next_action += 1;
100        let id = self.plugins.next_action;
101        let enabled: Vec<_> = packages
102            .iter()
103            .filter(|(_, enabled)| *enabled)
104            .map(|(p, _)| p)
105            .collect();
106        if !self
107            .runner
108            .submit_plugin_install(crate::runner::PluginInstallRequest {
109                id,
110                packages: json!(enabled),
111            })
112        {
113            return Err("runner does not support asynchronous plugin installation".into());
114        }
115        self.plugins.install_pending = Some(PendingInstall {
116            id,
117            revision: self.plugins.revision,
118            packages,
119        });
120        self.plugins.install_status = json!({"state":"pending","id":id});
121        self.pump();
122        Ok(id)
123    }
124
125    pub(super) fn defer_plugin_run(&mut self) -> bool {
126        if self.plugins.install_pending.is_none() {
127            return false;
128        }
129        self.plugins.deferred_run = true;
130        self.run_generation += 1;
131        true
132    }
133    pub(super) fn plugin_work_pending(&self) -> bool {
134        self.plugins.install_pending.is_some() || self.plugins.pending.is_some()
135    }
136
137    pub(super) fn plugin_installation_pending(&self) -> bool {
138        self.plugins.install_pending.is_some()
139    }
140
141    pub(super) fn plugin_runner_replaced(&mut self) {
142        if self.plugins.pending.take().is_some() {
143            self.plugin_result(Err(
144                "plugin transaction cancelled because runner changed".into()
145            ));
146        }
147        if let Some(pending) = &self.plugins.install_pending {
148            let enabled: Vec<_> = pending
149                .packages
150                .iter()
151                .filter(|(_, enabled)| *enabled)
152                .map(|(p, _)| p)
153                .collect();
154            if !self
155                .runner
156                .submit_plugin_install(crate::runner::PluginInstallRequest {
157                    id: pending.id,
158                    packages: json!(enabled),
159                })
160            {
161                self.plugins.install_pending = None;
162                self.plugins.install_status =
163                    json!({"state":"error","error":"replacement runner cannot validate plugins"});
164            }
165        }
166    }
167
168    pub fn plugin_installation_status(&self) -> Value {
169        self.plugins.install_status.clone()
170    }
171
172    pub(super) fn pump_plugin_installations(&mut self) {
173        while let Some(reply) = self.runner.poll_plugin_install() {
174            if !self
175                .plugins
176                .install_pending
177                .as_ref()
178                .is_some_and(|p| p.id == reply.id)
179            {
180                continue;
181            }
182            let pending = self.plugins.install_pending.take().unwrap();
183            let result = if pending.revision != self.plugins.revision {
184                Err("plugin installations changed during validation".to_string())
185            } else {
186                reply.result.and_then(|registry| {
187                    let enabled = pending
188                        .packages
189                        .iter()
190                        .filter(|(_, enabled)| *enabled)
191                        .map(|(p, _)| p.clone())
192                        .collect();
193                    Runtime::from_validated(enabled, registry)
194                })
195            };
196            let deferred = std::mem::take(&mut self.plugins.deferred_run);
197            match result {
198                Ok(runtime) => {
199                    self.plugins.runtime = Some(runtime);
200                    self.plugins.packages = pending.packages;
201                    self.plugins.revision = self.plugins.revision.wrapping_add(1);
202                    self.plugins.install_status = json!({"state":"complete","id":pending.id});
203                    self.plugins_changed();
204                }
205                Err(error) => {
206                    self.plugins.install_status =
207                        json!({"state":"error","id":pending.id,"error":error});
208                    if deferred {
209                        self.rerun_history();
210                    }
211                }
212            }
213        }
214    }
215
216    /// Restore explicitly installed/enabled packages, independently of loading a document.
217    pub fn set_plugin_packages(&mut self, packages: Vec<PackageBundle>) -> Result<(), String> {
218        self.plugins
219            .rebuild(packages.into_iter().map(|p| (p, true)).collect())?;
220        self.plugins_changed();
221        Ok(())
222    }
223
224    pub fn install_plugin(&mut self, package: Value) -> Result<Value, String> {
225        let package: PackageBundle =
226            serde_json::from_value(package).map_err(|e| format!("plugin package: {e}"))?;
227        let pin = package.pin()?;
228        let mut packages = self.plugins.packages.clone();
229        packages.retain(|(p, _)| p.pin().ok().as_ref() != Some(&pin));
230        packages.push((package, true));
231        self.plugins.rebuild(packages)?;
232        self.plugins_changed();
233        Ok(json!(pin))
234    }
235
236    pub fn installed_plugins(&self) -> Value {
237        Value::Array(
238            self.plugins
239                .packages
240                .iter()
241                .filter_map(|(p, enabled)| {
242                    p.pin().ok().map(|pin| {
243                        json!({"id":pin.id,"version":pin.version,"apiVersion":pin.api_version,
244                "digest":pin.digest,"name":p.manifest.name,"enabled":enabled})
245                    })
246                })
247                .collect(),
248        )
249    }
250
251    pub fn export_plugin(&self, id: &str, digest: &str) -> Result<Value, String> {
252        let (package, _) = self
253            .plugins
254            .packages
255            .iter()
256            .find(|(p, _)| {
257                p.pin()
258                    .is_ok_and(|pin| pin.id == id && pin.digest == digest)
259            })
260            .ok_or_else(|| format!("plugin {id} at {digest} is not installed"))?;
261        serde_json::to_value(package).map_err(|e| e.to_string())
262    }
263
264    pub fn enable_plugin(&mut self, id: &str, digest: &str, enabled: bool) -> Result<(), String> {
265        let mut packages = self.plugins.packages.clone();
266        let (_, state) = packages
267            .iter_mut()
268            .find(|(p, _)| {
269                p.pin()
270                    .is_ok_and(|pin| pin.id == id && pin.digest == digest)
271            })
272            .ok_or_else(|| format!("plugin {id} at {digest} is not installed"))?;
273        *state = enabled;
274        self.plugins.rebuild(packages)?;
275        self.plugins_changed();
276        Ok(())
277    }
278
279    pub fn remove_plugin(&mut self, id: &str, digest: &str) -> Result<(), String> {
280        let mut packages = self.plugins.packages.clone();
281        let before = packages.len();
282        packages.retain(|(p, _)| {
283            !p.pin()
284                .is_ok_and(|pin| pin.id == id && pin.digest == digest)
285        });
286        if packages.len() == before {
287            return Err(format!("plugin {id} at {digest} is not installed"));
288        }
289        self.plugins.rebuild(packages)?;
290        self.plugins_changed();
291        Ok(())
292    }
293
294    fn plugins_changed(&mut self) {
295        self.cancel_plugin_action();
296        self.runner.reset();
297        self.rerun_history();
298    }
299
300    fn saved_plugin_pins(&self) -> Result<Vec<PluginPin>, String> {
301        serde_json::from_value(
302            self.history
303                .document_block("plugins")
304                .cloned()
305                .unwrap_or_else(|| json!([])),
306        )
307        .map_err(|e| format!("document plugin pins: {e}"))
308    }
309
310    /// Existing document pins always win; an unpinned package uses the last explicit install.
311    fn available_plugin_pins(&self) -> Result<Vec<PluginPin>, String> {
312        let mut pins = self.saved_plugin_pins()?;
313        for (package, enabled) in self.plugins.packages.iter().rev() {
314            if !enabled {
315                continue;
316            }
317            let pin = package.pin()?;
318            if !pins.iter().any(|p| p.id == pin.id) {
319                pins.push(pin);
320            }
321        }
322        Ok(pins)
323    }
324
325    pub fn plugin_catalogue(&self) -> Value {
326        let mut features = Vec::new();
327        let mut actions = Vec::new();
328        let mut workbenches = Vec::new();
329        let mut panels = Vec::new();
330        let mut annotations = Vec::new();
331        let mut errors = Vec::new();
332        if let Some(runtime) = &self.plugins.runtime {
333            match self.available_plugin_pins() {
334                Ok(pins) => {
335                    for pin in pins {
336                        if self.installed_pin(&pin.id, &pin.digest).as_ref() != Ok(&pin) {
337                            errors.push(format!(
338                                "plugin {} at {} is missing or disabled",
339                                pin.id, pin.digest
340                            ));
341                            continue;
342                        }
343                        // One unavailable dependency must not hide healthy package controls.
344                        match runtime.registry().for_document(&[pin]) {
345                            Ok(registry) => {
346                                features.extend(registry.features().iter().map(|f| {
347                                let mut schema = f.input_params_schema.clone();
348                                schema["id"] = json!({"type":"string","default_value":null});
349                                json!({"type":f.id,"id":f.id,"label":f.label,"longName":f.label,"ribbonPath":f.ribbon_path,"commandSize":f.command_size,"icon":f.glyph,
350                                    "shortName":if f.short_name.is_empty() { f.id.rsplit('/').next().unwrap_or("Plugin") } else { &f.short_name },
351                                    "inputParamsSchema":schema,"package":f.package})
352                            }));
353                                panels.extend(registry.panels().iter().map(|p| json!(p)));
354                                annotations.extend(registry.annotations().iter().map(|a| {
355                                    let mut schema = a.input_params_schema.clone();
356                                    schema["id"] = json!({"type":"string","default_value":null});
357                                    json!({"type":a.id,"id":a.id,"label":a.label,"longName":a.label,
358                                        "shortName":"ANN","inputParamsSchema":schema,"package":a.package})
359                                }));
360                                actions.extend(registry.actions().iter().map(|a| json!(a)));
361                                workbenches.extend(registry.workbenches().iter().map(|w| json!(w)));
362                            }
363                            Err(error) => errors.push(error),
364                        }
365                    }
366                }
367                Err(error) => errors.push(error),
368            }
369        } else if let Ok(pins) = self.saved_plugin_pins() {
370            errors.extend(pins.iter().map(|p| {
371                format!(
372                    "plugin {} at {} is not installed or enabled",
373                    p.id, p.digest
374                )
375            }));
376        }
377        json!({"revision":self.plugins.revision,"documentRevision":self.history.revision(),
378            "features":features,"actions":actions,"workbenches":workbenches,"panels":panels,"annotations":annotations,"errors":errors})
379    }
380
381    pub fn plugin_workbenches(&self) -> Value {
382        self.plugin_catalogue()["workbenches"].clone()
383    }
384
385    /// Engine-local schema overlay; never adds package schemas to the process-wide built-ins.
386    pub fn feature_catalogue(&self) -> Value {
387        let mut catalogue = crate::features::feature_catalogue();
388        if let Some(features) = catalogue["features"].as_array_mut() {
389            features.extend(
390                self.plugin_catalogue()["features"]
391                    .as_array()
392                    .into_iter()
393                    .flatten()
394                    .cloned(),
395            );
396        }
397        catalogue
398    }
399    pub fn feature_schema(&self, feature_type: &str) -> Option<Value> {
400        crate::features::feature_schema(feature_type).or_else(|| {
401            self.plugin_catalogue()["features"]
402                .as_array()?
403                .iter()
404                .find(|f| f["type"] == feature_type)
405                .cloned()
406        })
407    }
408    pub fn feature_form_fields(&self, feature_type: &str) -> Vec<crate::style::FormField> {
409        if let Some(schema) = crate::features::feature_schema(feature_type) {
410            return crate::features::form_fields_from_schema(&schema);
411        }
412        self.feature_schema(feature_type)
413            .as_ref()
414            .map(crate::features::plugin_form_fields_from_schema)
415            .unwrap_or_default()
416    }
417    pub fn feature_default_params(&self, feature_type: &str) -> Value {
418        let mut params = serde_json::Map::new();
419        if let Some(schema) = self.feature_schema(feature_type) {
420            if let Some(props) = schema["inputParamsSchema"].as_object() {
421                for (name, spec) in props {
422                    params.insert(name.clone(), spec["default_value"].clone());
423                }
424            }
425        }
426        Value::Object(params)
427    }
428
429    /// Selecting a first pin is undoable; replacing an existing pin requires migration.
430    pub fn select_plugin(&mut self, id: &str, digest: &str) -> Result<(), String> {
431        self.require_plugin_editable()?;
432        let pin = self.installed_pin(id, digest)?;
433        let mut pins = self.saved_plugin_pins()?;
434        if let Some(old) = pins.iter().find(|p| p.id == id) {
435            return if old == &pin {
436                Ok(())
437            } else {
438                Err("use migrate_plugin to validate a version change".into())
439            };
440        }
441        pins.push(pin);
442        self.history
443            .set_document_block("plugins", Some(json!(pins)), None);
444        self.rerun_history();
445        Ok(())
446    }
447
448    fn installed_pin(&self, id: &str, digest: &str) -> Result<PluginPin, String> {
449        self.plugins
450            .packages
451            .iter()
452            .filter(|(_, enabled)| *enabled)
453            .find_map(|(p, _)| {
454                p.pin()
455                    .ok()
456                    .filter(|pin| pin.id == id && pin.digest == digest)
457            })
458            .ok_or_else(|| format!("plugin {id} at {digest} is not installed and enabled"))
459    }
460    fn require_plugin_editable(&self) -> Result<(), String> {
461        if self.plugins.install_pending.is_some() {
462            return Err("wait for plugin installation validation".into());
463        }
464        if let Some(reason) = self.history.locked() {
465            return Err(format!("the document is read-only: {reason}"));
466        }
467        if self.plugins.pending.is_some() {
468            return Err("a plugin transaction is already pending".into());
469        }
470        if self.run_pending() {
471            return Err("wait for the current history recompute".into());
472        }
473        Ok(())
474    }
475
476    pub fn migrate_plugin(&mut self, id: &str, digest: &str) -> Result<u64, String> {
477        self.require_plugin_editable()?;
478        let pin = self.installed_pin(id, digest)?;
479        let mut pins = self.saved_plugin_pins()?;
480        pins.retain(|p| p.id != id);
481        pins.push(pin);
482        let pending = self.new_plugin_pending(pins.clone());
483        let action_id = pending.id;
484        let mut staged = self.history.clone();
485        staged.set_document_block_no_undo("plugins", Some(json!(pins)));
486        self.plugins.pending = Some(pending);
487        if let Err(error) = self.submit_plugin_candidate(staged) {
488            self.plugins.pending = None;
489            return Err(error);
490        }
491        self.pump();
492        Ok(action_id)
493    }
494
495    fn new_plugin_pending(&mut self, pins: Vec<PluginPin>) -> PendingAction {
496        self.plugins.next_action += 1;
497        PendingAction {
498            id: self.plugins.next_action,
499            revision: self.history.revision(),
500            registry_revision: self.plugins.revision,
501            selection: self.selection_json(),
502            generation: self.run_generation,
503            pins,
504            staged: None,
505            notifications: Vec::new(),
506            annotation_id: None,
507            allowed_annotation_errors: Vec::new(),
508        }
509    }
510
511    pub fn submit_plugin_action(&mut self, id: &str, params: Value) -> Result<(), String> {
512        let selection = serde_json::from_str(&self.selection_json()).unwrap_or(Value::Null);
513        self.plugin_action(id, params, selection).map(|_| ())
514    }
515
516    /// Submit a callback; neither callback commands nor recompute touch live history.
517    pub fn plugin_action(
518        &mut self,
519        id: &str,
520        params: Value,
521        selection: Value,
522    ) -> Result<u64, String> {
523        self.require_plugin_editable()?;
524        let pins = self.available_plugin_pins()?;
525        let runtime = self
526            .plugins
527            .runtime
528            .as_ref()
529            .ok_or("no plugins installed")?;
530        let registry = runtime.registry().for_document(&pins)?;
531        if !registry.actions().iter().any(|a| a.id == id) {
532            return Err(format!("plugin action {id} is unavailable"));
533        }
534        let pending = self.new_plugin_pending(pins.clone());
535        let action_id = pending.id;
536        let input = json!({"params":params,"selection":selection,
537            "document":serde_json::from_str::<Value>(&self.history.request_json()).map_err(|e| e.to_string())?});
538        self.sync_plugin_runner();
539        let request = crate::runner::PluginActionRequest {
540            id: action_id,
541            pins,
542            action: id.into(),
543            input,
544            script: None,
545        };
546        if !self.runner.submit_plugin_action(request) {
547            return Err("the installed runner does not support plugin actions".into());
548        }
549        self.plugins.pending = Some(pending);
550        self.plugins.status = json!({"state":"pending","id":action_id});
551        self.pump();
552        Ok(action_id)
553    }
554
555    pub fn plugin_action_status(&self) -> Value {
556        self.plugins.status.clone()
557    }
558
559    /// Run an editor script through the same worker and atomic undo transaction
560    /// as named actions, without installing the editor source as a dependency.
561    pub fn run_javascript(&mut self, source: &str) -> Result<u64, String> {
562        self.require_plugin_editable()?;
563        if source.len() > 1024 * 1024 {
564            return Err("script exceeds 1 MiB".into());
565        }
566        let pins = self.available_plugin_pins()?;
567        let pending = self.new_plugin_pending(pins.clone());
568        let id = pending.id;
569        let input = json!({"params":{},
570            "selection":serde_json::from_str::<Value>(&self.selection_json()).unwrap_or(Value::Null),
571            "document":serde_json::from_str::<Value>(&self.history.request_json()).map_err(|e| e.to_string())?});
572        self.sync_plugin_runner();
573        let request = crate::runner::PluginActionRequest {
574            id, pins, action: String::new(), input, script: Some(source.into()),
575        };
576        if !self.runner.submit_plugin_action(request) {
577            return Err("the installed runner does not support JavaScript execution".into());
578        }
579        self.plugins.pending = Some(pending);
580        self.plugins.status = json!({"state":"pending","id":id});
581        self.pump();
582        Ok(id)
583    }
584    pub fn poll_plugin_action(&mut self) -> Option<Result<Value, String>> {
585        self.pump();
586        self.plugins.results.pop_front()
587    }
588    pub fn cancel_plugin_action(&mut self) -> bool {
589        if self.plugins.pending.take().is_none() {
590            return false;
591        }
592        self.plugin_result(Err("plugin transaction cancelled".into()));
593        // Queued old outputs can no longer apply. Restore the live document's baseline.
594        self.run_generation += 1;
595        self.applied_generation = self.run_generation;
596        self.runner.reset();
597        self.rerun_history();
598        true
599    }
600
601    pub(super) fn sync_plugin_runner(&mut self) {
602        let plugins = &self.plugins;
603        self.runner
604            .sync_plugins(plugins.revision, &mut || json!(plugins.bundles()));
605    }
606
607    fn plugin_result(&mut self, result: Result<Value, String>) {
608        self.plugins.status = match &result {
609            Ok(value) => json!({"state":"complete","result":value}),
610            Err(error) => json!({"state":"error","error":error}),
611        };
612        self.plugins.results.push_back(result);
613    }
614
615    fn plugin_pending_valid(&self, pending: &PendingAction) -> bool {
616        pending.revision == self.history.revision()
617            && pending.registry_revision == self.plugins.revision
618            && pending.selection == self.selection_json()
619            && pending.generation == self.run_generation
620            && self.history.locked().is_none()
621    }
622
623    pub(super) fn pump_plugin_callbacks(&mut self) {
624        if self
625            .plugins
626            .pending
627            .as_ref()
628            .is_some_and(|p| !self.plugin_pending_valid(p))
629        {
630            let staged = self.plugins.pending.take().unwrap().staged.is_some();
631            self.plugin_result(Err(
632                "plugin transaction invalidated by a document or selection change".into(),
633            ));
634            if staged {
635                self.runner.reset();
636                self.rerun_history();
637            }
638        }
639        while let Some(reply) = self.runner.poll_plugin_action() {
640            let Some(pending) = self.plugins.pending.as_ref() else {
641                continue;
642            };
643            if pending.id != reply.id {
644                continue;
645            }
646            if !self.plugin_pending_valid(pending) {
647                self.plugins.pending = None;
648                self.plugin_result(Err(
649                    "plugin transaction invalidated by a document or selection change".into(),
650                ));
651                continue;
652            }
653            if let Ok(plan) = &reply.result {
654                if plan.commands.is_empty() {
655                    self.plugins.pending = None;
656                    self.plugin_result(Ok(
657                        json!({"id":reply.id,"notifications":plan.notifications}),
658                    ));
659                    continue;
660                }
661            }
662            let staged = reply.result.and_then(|plan| {
663                let mut staged = self.history.clone();
664                let pending = self.plugins.pending.as_mut().unwrap();
665                staged.set_document_block_no_undo("plugins", Some(json!(pending.pins)));
666                pending.notifications = plan.notifications;
667                // Use the serializable command contract to keep all validation at one door.
668                let commands = serde_json::to_value(plan.commands).map_err(|e| e.to_string())?;
669                for command in commands.as_array().ok_or("invalid action commands")? {
670                    match command["op"].as_str().unwrap_or("") {
671                        "addFeature" => {
672                            let ty = command["featureType"].as_str().ok_or("addFeature requires featureType")?;
673                            let schema = self.feature_schema(ty).ok_or_else(|| format!("unknown feature type {ty}"))?;
674                            let mut params = self.feature_default_params(ty);
675                            let supplied = command["inputParams"].as_object().ok_or("feature parameters must be an object")?;
676                            params.as_object_mut().unwrap().extend(supplied.clone());
677                            let id = staged.next_feature_id(schema["shortName"].as_str().unwrap_or(ty));
678                            params["id"] = json!(id);
679                            staged.push_feature(json!({"type":ty,"inputParams":params,"persistentData":command["persistentData"]}));
680                        }
681                        "updateFeature" => {
682                            let id = command["id"].as_str().ok_or("updateFeature requires id")?;
683                            let index = staged.index_of(id).ok_or_else(|| format!("feature {id} not found"))?;
684                            let mut params = command["inputParams"].clone();
685                            if !params.is_object() { return Err("feature parameters must be an object".into()); }
686                            params["id"] = json!(id);
687                            staged.set_feature_params(index, params);
688                        }
689                        "deleteFeature" => {
690                            let id = command["id"].as_str().ok_or("deleteFeature requires id")?;
691                            let index = staged.index_of(id).ok_or_else(|| format!("feature {id} not found"))?;
692                            staged.remove_feature(index);
693                        }
694                        op => return Err(format!("unsupported plugin document command {op}")),
695                    }
696                }
697                Ok(staged)
698            });
699            match staged.and_then(|staged| self.submit_plugin_candidate(staged)) {
700                Ok(()) => {}
701                Err(error) => {
702                    self.plugins.pending = None;
703                    self.plugin_result(Err(error));
704                }
705            }
706        }
707    }
708
709    fn submit_plugin_candidate(&mut self, mut staged: History) -> Result<(), String> {
710        staged.set_rollback(staged.len().saturating_sub(1));
711        let mut request: brep_kernel::HistoryRequest =
712            serde_json::from_value(staged.prefix_request()).map_err(|e| e.to_string())?;
713        request.display_lod = self.settings.lod_factor;
714        self.run_generation += 1;
715        let pending = self
716            .plugins
717            .pending
718            .as_mut()
719            .ok_or("plugin transaction disappeared")?;
720        pending.generation = self.run_generation;
721        pending.staged = Some(staged);
722        self.plugins.status = json!({"state":"pending","id":pending.id});
723        // Full delta means discarding an unsuccessful candidate cannot poison the display baseline.
724        self.runner.reset();
725        self.sync_plugin_runner();
726        self.runner.sync_parts_library(
727            brep_kernel::parts_library_revision(),
728            &mut brep_kernel::parts_library_map,
729        );
730        self.runner.submit_run(request, self.run_generation);
731        Ok(())
732    }
733
734    /// Returns false when this candidate must be discarded, without applying its scene.
735    pub(super) fn accept_plugin_run(&mut self, reply: &crate::runner::RunReply) -> bool {
736        let Some(pending) = self.plugins.pending.as_ref() else {
737            return true;
738        };
739        if pending.staged.is_none() || pending.generation != reply.generation {
740            return true;
741        }
742        let valid = self.plugin_pending_valid(pending);
743        let mut pending = self.plugins.pending.take().unwrap();
744        let report = &reply.output.report;
745        let annotation_errors: Vec<_> = annotation_failures(report.pmi.as_ref())
746            .into_iter()
747            .filter(|failure| !pending.allowed_annotation_errors.contains(failure))
748            .map(|failure| format!("{}: {}", failure.id, failure.message))
749            .collect();
750        let result = if !valid {
751            Err("plugin transaction invalidated by a document or selection change".into())
752        } else if !report.feature_errors.is_empty()
753            || !report.unresolved.is_empty()
754            || !report.display_errors.is_empty()
755            || !annotation_errors.is_empty()
756        {
757            Err(format!(
758                "plugin transaction recompute failed: {}",
759                report
760                    .feature_errors
761                    .iter()
762                    .chain(&report.unresolved)
763                    .chain(&report.display_errors)
764                    .chain(&annotation_errors)
765                    .cloned()
766                    .collect::<Vec<_>>()
767                    .join("; ")
768            ))
769        } else {
770            fold_annotation_data(pending.staged.as_mut().unwrap(), report.pmi.as_ref());
771            self.history
772                .commit_staged(pending.staged.as_ref().unwrap())
773                .map(|_| json!({"id":pending.id,"notifications":pending.notifications,"annotationId":pending.annotation_id}))
774        };
775        let accepted = result.is_ok();
776        self.plugin_result(result);
777        if !accepted {
778            self.runner.reset();
779            self.rerun_history();
780        }
781        accepted
782    }
783
784    /// Add package pins and features together so undo restores both.
785    pub(super) fn append_plugin_features(&mut self, features: &[Value]) -> Result<bool, String> {
786        if !features
787            .iter()
788            .any(|f| f["type"].as_str().is_some_and(|t| t.contains('/')))
789        {
790            return Ok(false);
791        }
792        if let Some(reason) = self.history.locked() {
793            return Err(format!("the document is read-only: {reason}"));
794        }
795        let mut pins = self.saved_plugin_pins()?;
796        for feature in features {
797            let ty = feature["type"].as_str().unwrap_or("");
798            if !ty.contains('/') {
799                continue;
800            }
801            let schema = self
802                .feature_schema(ty)
803                .ok_or_else(|| format!("plugin feature {ty} is unavailable"))?;
804            let pin: PluginPin =
805                serde_json::from_value(schema["package"].clone()).map_err(|e| e.to_string())?;
806            if !pins.iter().any(|p| p.id == pin.id) {
807                pins.push(pin);
808            }
809        }
810        let mut staged = self.history.clone();
811        staged.set_document_block_no_undo("plugins", Some(json!(pins)));
812        staged.push_features(features.to_vec());
813        staged.set_rollback(staged.len().saturating_sub(1));
814        self.history.commit_staged(&staged)?;
815        Ok(true)
816    }
817
818    pub(super) fn plugin_export_ready(&self) -> Result<(), String> {
819        fn uses_plugins(value: &Value) -> bool {
820            match value {
821                Value::Object(map) => {
822                    map.get("plugins")
823                        .and_then(Value::as_array)
824                        .is_some_and(|p| !p.is_empty())
825                        || map
826                            .get("type")
827                            .and_then(Value::as_str)
828                            .is_some_and(|ty| ty.contains('/'))
829                        || map.values().any(uses_plugins)
830                }
831                Value::Array(values) => values.iter().any(uses_plugins),
832                _ => false,
833            }
834        }
835        let document: Value =
836            serde_json::from_str(&self.history.request_json()).map_err(|e| e.to_string())?;
837        if !uses_plugins(&document) {
838            return Ok(());
839        }
840        if self.run_pending()
841            || self.plugins.pending.is_some()
842            || self.plugins.install_pending.is_some()
843        {
844            return Err("plugin geometry is pending; wait for recompute before export".into());
845        }
846        let annotation_errors = plugin_annotation_errors(self.pmi_report.as_ref());
847        if !annotation_errors.is_empty() {
848            return Err(format!(
849                "unavailable plugin annotations: {}",
850                annotation_errors.join("; ")
851            ));
852        }
853        let report: Value = serde_json::from_str(&self.history_report).unwrap_or(Value::Null);
854        if report.get("error").is_some()
855            || ["featureErrors", "unresolved", "displayErrors"]
856                .iter()
857                .any(|key| {
858                    report[*key]
859                        .as_array()
860                        .is_some_and(|items| !items.is_empty())
861                })
862        {
863            return Err("plugin document has failed or unavailable geometry; recompute successfully before export".into());
864        }
865        Ok(())
866    }
867
868    pub(super) fn plugin_resident_handles(
869        &self,
870        request: &brep_kernel::HistoryRequest,
871    ) -> Result<Vec<(String, u32)>, String> {
872        let result = self.execute_plugin_history(request);
873        let mut names = Vec::new();
874        let mut handles = std::collections::HashMap::new();
875        for feature in result.results {
876            if let Some(error) = feature.error {
877                return Err(format!("{}: {error}", feature.id));
878            }
879            for removed in feature.removed {
880                handles.remove(&removed);
881                names.retain(|n| n != &removed);
882            }
883            for added in feature.added {
884                if handles.insert(added.name.clone(), added.handle).is_none() {
885                    names.push(added.name);
886                }
887            }
888        }
889        Ok(names
890            .into_iter()
891            .map(|name| {
892                let handle = handles[&name];
893                (name, handle)
894            })
895            .collect())
896    }
897
898    pub(super) fn inherit_plugins(&mut self, parent: &Self) {
899        self.plugins.packages = parent.plugins.packages.clone();
900        self.plugins.runtime = parent.plugins.runtime.clone();
901        self.plugins.revision = parent.plugins.revision;
902    }
903
904    pub(super) fn with_plugin_provider<R>(&self, f: impl FnOnce() -> R) -> R {
905        match &self.plugins.runtime {
906            Some(runtime) => runtime.with_provider(f),
907            None => f(),
908        }
909    }
910
911    pub(super) fn execute_plugin_history(
912        &self,
913        request: &brep_kernel::HistoryRequest,
914    ) -> brep_kernel::HistoryResult {
915        match &self.plugins.runtime {
916            Some(runtime) => runtime.execute_history(request),
917            None => brep_kernel::execute_history(request),
918        }
919    }
920
921    pub(super) fn run_plugin_scene(
922        &mut self,
923        request: &brep_kernel::HistoryRequest,
924    ) -> Result<crate::pipeline::SceneBuildReport, String> {
925        match &self.plugins.runtime {
926            Some(runtime) => runtime.with_provider(|| {
927                crate::pipeline::update_scene_from_history(&mut self.scene, request)
928            }),
929            None => crate::pipeline::update_scene_from_history(&mut self.scene, request),
930        }
931    }
932}
933
934
935/// The exact error identity used by repair transactions. Other operations stay strict.
936#[derive(Clone, Debug, PartialEq, Eq)]
937struct AnnotationFailure {
938    view_id: String,
939    id: String,
940    kind: String,
941    message: String,
942}
943
944fn annotation_failures(report: Option<&brep_kernel::PmiReport>) -> Vec<AnnotationFailure> {
945    report
946        .into_iter()
947        .flat_map(|r| &r.views)
948        .flat_map(|view| {
949            view.annotations
950                .iter()
951                .filter(|a| {
952                    a.enabled && a.kind.contains('/') && a.status == brep_kernel::PmiStatus::Error
953                })
954                .map(|a| AnnotationFailure {
955                    view_id: view.id.clone(),
956                    id: a.id.clone(),
957                    kind: a.kind.clone(),
958                    message: a.message.clone(),
959                })
960        })
961        .collect()
962}
963
964/// Even tolerated repair errors continue to block exports.
965pub(super) fn plugin_annotation_errors(report: Option<&brep_kernel::PmiReport>) -> Vec<String> {
966    annotation_failures(report)
967        .into_iter()
968        .map(|a| format!("{}: {}", a.id, a.message))
969        .collect()
970}
971
972fn fold_annotation_data(history: &mut History, report: Option<&brep_kernel::PmiReport>) {
973    let Some(block) = history.pmi_block() else {
974        return;
975    };
976    let Ok(mut state) = serde_json::from_value::<brep_kernel::PmiState>(block.clone()) else {
977        return;
978    };
979    let mut changed = false;
980    for row in report
981        .into_iter()
982        .flat_map(|r| &r.views)
983        .flat_map(|v| &v.annotations)
984    {
985        if row.status != brep_kernel::PmiStatus::Ok || !row.kind.contains('/') {
986            continue;
987        }
988        if let (Some(data), Some(annotation)) =
989            (&row.persistent_data, state.find_annotation_mut(&row.id))
990        {
991            if annotation.kind == row.kind
992                && (annotation.persistent_data != *data
993                    || annotation.plugin_replay != row.plugin_replay)
994            {
995                annotation.persistent_data = data.clone();
996                annotation.plugin_replay = row.plugin_replay.clone();
997                changed = true;
998            }
999        }
1000    }
1001    if changed {
1002        history.set_pmi_block_no_undo(Some(json!(state)));
1003    }
1004}
1005
1006impl EngineState {
1007    pub fn plugin_panels(&self) -> Value {
1008        self.plugin_catalogue()["panels"].clone()
1009    }
1010
1011    pub fn plugin_annotation_catalogue(&self) -> Value {
1012        self.plugin_catalogue()["annotations"].clone()
1013    }
1014
1015    /// Built-ins and document-scoped providers share the normal annotation form shape.
1016    pub fn annotation_schema(&self, kind: &str) -> Option<Value> {
1017        brep_kernel::pmi_type(kind)
1018            .map(|d| (d.schema)())
1019            .or_else(|| {
1020                self.plugin_annotation_catalogue()
1021                    .as_array()?
1022                    .iter()
1023                    .find(|a| a["type"] == kind)
1024                    .cloned()
1025            })
1026    }
1027
1028    pub(super) fn fold_plugin_annotation_data(&mut self) {
1029        if self.expression_preview.is_none() {
1030            fold_annotation_data(&mut self.history, self.pmi_report.as_ref());
1031        }
1032    }
1033
1034    pub fn plugin_add_annotation(
1035        &mut self,
1036        view_id: &str,
1037        kind: &str,
1038        mut params: Value,
1039    ) -> Result<u64, String> {
1040        self.require_plugin_editable()?;
1041        let schema = self
1042            .plugin_annotation_catalogue()
1043            .as_array()
1044            .into_iter()
1045            .flatten()
1046            .find(|s| s["type"] == kind)
1047            .cloned()
1048            .ok_or_else(|| format!("unavailable plugin annotation {kind}"))?;
1049        if !params.is_object() {
1050            return Err("annotation parameters must be an object".into());
1051        }
1052        let mut state = self.pmi_state();
1053        let id = state.next_id("ANN");
1054        params["id"] = json!(id);
1055        state
1056            .find_view_mut(view_id)
1057            .ok_or_else(|| format!("no PMI view '{view_id}'"))?
1058            .annotations
1059            .push(brep_kernel::PmiAnnotation {
1060                kind: kind.into(),
1061                enabled: true,
1062                params,
1063                persistent_data: Value::Null,
1064                plugin_replay: None,
1065                label_world: None,
1066            });
1067        let pin: PluginPin =
1068            serde_json::from_value(schema["package"].clone()).map_err(|e| e.to_string())?;
1069        let mut pins = self.saved_plugin_pins()?;
1070        if let Some(old) = pins.iter().find(|p| p.id == pin.id) {
1071            if old != &pin {
1072                return Err("annotation package does not match document pin".into());
1073            }
1074        } else {
1075            pins.push(pin);
1076        }
1077        self.submit_annotation_state(state, pins, id, false)
1078    }
1079
1080    pub fn plugin_update_annotation(&mut self, id: &str, mut params: Value) -> Result<u64, String> {
1081        self.require_plugin_editable()?;
1082        if !params.is_object() {
1083            return Err("annotation parameters must be an object".into());
1084        }
1085        let mut state = self.pmi_state();
1086        let annotation = state
1087            .find_annotation_mut(id)
1088            .ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1089        if !annotation.kind.contains('/') {
1090            return Err("annotation is not plugin-owned".into());
1091        }
1092        params["id"] = json!(id);
1093        annotation.params = params;
1094        self.submit_annotation_state(state, self.saved_plugin_pins()?, id.into(), false)
1095    }
1096
1097    pub fn plugin_delete_annotation(&mut self, id: &str) -> Result<u64, String> {
1098        self.require_plugin_editable()?;
1099        let mut state = self.pmi_state();
1100        let (v, a) = state
1101            .locate_annotation(id)
1102            .ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1103        if !state.views[v].annotations[a].kind.contains('/') {
1104            return Err("annotation is not plugin-owned".into());
1105        }
1106        state.views[v].annotations.remove(a);
1107        self.submit_annotation_state(state, self.saved_plugin_pins()?, id.into(), true)
1108    }
1109
1110    pub fn plugin_set_annotation_enabled(
1111        &mut self,
1112        id: &str,
1113        enabled: bool,
1114    ) -> Result<u64, String> {
1115        self.require_plugin_editable()?;
1116        let mut state = self.pmi_state();
1117        let annotation = state
1118            .find_annotation_mut(id)
1119            .ok_or_else(|| format!("no PMI annotation '{id}'"))?;
1120        if !annotation.kind.contains('/') {
1121            return Err("annotation is not plugin-owned".into());
1122        }
1123        annotation.enabled = enabled;
1124        self.submit_annotation_state(state, self.saved_plugin_pins()?, id.into(), !enabled)
1125    }
1126
1127    fn submit_annotation_state(
1128        &mut self,
1129        state: brep_kernel::PmiState,
1130        pins: Vec<PluginPin>,
1131        annotation_id: String,
1132        repair: bool,
1133    ) -> Result<u64, String> {
1134        let mut pending = self.new_plugin_pending(pins.clone());
1135        if repair {
1136            let baseline = self.pmi_state();
1137            pending.allowed_annotation_errors = annotation_failures(self.pmi_report.as_ref())
1138                .into_iter()
1139                .filter(|failure| {
1140                    let old = baseline
1141                        .find_view(&failure.view_id)
1142                        .and_then(|v| v.annotations.iter().find(|a| a.id() == failure.id));
1143                    let survivor = state
1144                        .find_view(&failure.view_id)
1145                        .and_then(|v| v.annotations.iter().find(|a| a.id() == failure.id));
1146                    old.is_some() && old == survivor
1147                })
1148                .collect();
1149        }
1150        let id = pending.id;
1151        pending.annotation_id = Some(annotation_id);
1152        let mut staged = self.history.clone();
1153        staged.set_pmi_block_no_undo(Some(json!(state)));
1154        staged.set_document_block_no_undo("plugins", Some(json!(pins)));
1155        self.plugins.pending = Some(pending);
1156        if let Err(error) = self.submit_plugin_candidate(staged) {
1157            self.plugins.pending = None;
1158            return Err(error);
1159        }
1160        self.pump();
1161        Ok(id)
1162    }
1163}