Skip to main content

bijux_cli/contracts/
operator_surface_contracts.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5/// Compact root help entrypoint contract for operator-first discovery.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
7pub struct CompactHelpEntryPointV1 {
8    /// Core built-in root routes shown to operators.
9    pub built_in_routes: Vec<String>,
10    /// Mounted official app root routes.
11    pub mounted_app_routes: Vec<String>,
12    /// Plugin namespace routes.
13    pub plugin_routes: Vec<String>,
14    /// Suggested next commands from the root screen.
15    pub next_commands: Vec<String>,
16}
17
18/// Build compact root help with required operator routes and deterministic ordering.
19pub fn build_compact_operator_help_entrypoint(
20    built_in_routes: &[&str],
21    mounted_app_routes: &[&str],
22    plugin_routes: &[&str],
23    next_commands: &[&str],
24) -> Result<CompactHelpEntryPointV1, String> {
25    let normalized_built_ins = unique_sorted_non_empty(built_in_routes)?;
26    for required in ["dag", "config", "doctor", "plugins"] {
27        if !normalized_built_ins.iter().any(|entry| entry == required) {
28            return Err(format!("built_in_routes missing required root command `{required}`"));
29        }
30    }
31    Ok(CompactHelpEntryPointV1 {
32        built_in_routes: normalized_built_ins,
33        mounted_app_routes: unique_sorted_non_empty(mounted_app_routes)?,
34        plugin_routes: unique_sorted_non_empty(plugin_routes)?,
35        next_commands: unique_sorted_non_empty(next_commands)?,
36    })
37}
38
39fn unique_sorted_non_empty(values: &[&str]) -> Result<Vec<String>, String> {
40    let mut normalized: Vec<String> = values
41        .iter()
42        .map(|value| value.trim())
43        .filter(|value| !value.is_empty())
44        .map(ToString::to_string)
45        .collect();
46    normalized.sort();
47    normalized.dedup();
48    if normalized.is_empty() {
49        return Err("route/help collections cannot be empty".to_string());
50    }
51    Ok(normalized)
52}
53
54/// Script-stable command envelope contract shared across runtime and bridge entrypoints.
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
56pub struct ScriptStableCommandEnvelopeV1 {
57    /// Schema version for stable machine parsing.
58    pub schema_version: String,
59    /// Canonical command path string.
60    pub command: String,
61    /// Success marker for script decisions.
62    pub ok: bool,
63    /// Stable command result code.
64    pub code: String,
65    /// Command payload.
66    pub data: Value,
67    /// Non-fatal warnings.
68    pub warnings: Vec<String>,
69    /// Fatal errors (empty on success).
70    pub errors: Vec<String>,
71}
72
73/// Build script-stable command envelope with exact required fields.
74pub fn build_script_stable_command_envelope(
75    schema_version: &str,
76    command: &str,
77    ok: bool,
78    code: &str,
79    data: Value,
80    warnings: Vec<String>,
81    errors: Vec<String>,
82) -> Result<ScriptStableCommandEnvelopeV1, String> {
83    if schema_version.trim().is_empty() {
84        return Err("schema_version cannot be empty".to_string());
85    }
86    if command.trim().is_empty() {
87        return Err("command cannot be empty".to_string());
88    }
89    if code.trim().is_empty() {
90        return Err("code cannot be empty".to_string());
91    }
92    if ok && !errors.is_empty() {
93        return Err("success envelope cannot contain errors".to_string());
94    }
95    if !ok && errors.is_empty() {
96        return Err("failure envelope must contain at least one error".to_string());
97    }
98    Ok(ScriptStableCommandEnvelopeV1 {
99        schema_version: schema_version.to_string(),
100        command: command.to_string(),
101        ok,
102        code: code.to_string(),
103        data,
104        warnings,
105        errors,
106    })
107}
108
109/// Stable actionable failure classes for CLI/runtime failures.
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
111#[serde(rename_all = "snake_case")]
112pub enum ActionableFailureClassV1 {
113    Parse,
114    Config,
115    Plugin,
116    Dag,
117    Io,
118    Runtime,
119}
120
121/// Useful error envelope with explicit remediation and evidence pointers.
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
123pub struct ActionableErrorEnvelopeV1 {
124    /// Failure class for coarse machine triage.
125    pub failure_class: ActionableFailureClassV1,
126    /// Stable error code.
127    pub code: String,
128    /// Human-readable failure message.
129    pub message: String,
130    /// Concrete remediation action for operators.
131    pub remediation: String,
132    /// Optional evidence pointer for logs or run artifacts.
133    pub evidence_pointer: Option<String>,
134}
135
136/// Build actionable error envelope with required diagnostic fields.
137pub fn build_actionable_error_envelope(
138    failure_class: ActionableFailureClassV1,
139    code: &str,
140    message: &str,
141    remediation: &str,
142    evidence_pointer: Option<&str>,
143) -> Result<ActionableErrorEnvelopeV1, String> {
144    if code.trim().is_empty() {
145        return Err("code cannot be empty".to_string());
146    }
147    if message.trim().is_empty() {
148        return Err("message cannot be empty".to_string());
149    }
150    if remediation.trim().is_empty() {
151        return Err("remediation cannot be empty".to_string());
152    }
153    if evidence_pointer.is_some_and(|value| value.trim().is_empty()) {
154        return Err("evidence_pointer cannot be blank when present".to_string());
155    }
156    Ok(ActionableErrorEnvelopeV1 {
157        failure_class,
158        code: code.to_string(),
159        message: message.to_string(),
160        remediation: remediation.to_string(),
161        evidence_pointer: evidence_pointer.map(ToString::to_string),
162    })
163}
164
165/// Explain contract with route target and runtime requirements.
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
167pub struct CommandExplainV1 {
168    /// Command path being explained.
169    pub command: String,
170    /// Route target key used by dispatch.
171    pub route_target: String,
172    /// Handler source (`builtin`, `official-app`, `plugin`).
173    pub handler_source: String,
174    /// Output schema version identifier.
175    pub output_schema: String,
176    /// Side-effect class (`read-only`, `writes-config`, `writes-run`, `executes-adapter`, `destructive`).
177    pub side_effect_class: String,
178    /// Required config keys for successful execution.
179    pub required_config_keys: Vec<String>,
180}
181
182/// Build command explain record for built-ins, official app routes, and plugins.
183pub fn build_command_explain_record(
184    command: &str,
185    route_target: &str,
186    handler_source: &str,
187    output_schema: &str,
188    side_effect_class: &str,
189    required_config_keys: &[&str],
190) -> Result<CommandExplainV1, String> {
191    for (field, value) in [
192        ("command", command),
193        ("route_target", route_target),
194        ("handler_source", handler_source),
195        ("output_schema", output_schema),
196        ("side_effect_class", side_effect_class),
197    ] {
198        if value.trim().is_empty() {
199            return Err(format!("{field} cannot be empty"));
200        }
201    }
202    let mut required: Vec<String> = required_config_keys
203        .iter()
204        .map(|value| value.trim())
205        .filter(|value| !value.is_empty())
206        .map(ToString::to_string)
207        .collect();
208    required.sort();
209    required.dedup();
210    Ok(CommandExplainV1 {
211        command: command.to_string(),
212        route_target: route_target.to_string(),
213        handler_source: handler_source.to_string(),
214        output_schema: output_schema.to_string(),
215        side_effect_class: side_effect_class.to_string(),
216        required_config_keys: required,
217    })
218}
219
220/// Output mode matrix row for one command.
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
222pub struct OutputModeParityEntryV1 {
223    /// Command path key.
224    pub command: String,
225    /// Supported output modes.
226    pub supported_modes: Vec<String>,
227}
228
229/// Output mode parity report across built-in and mounted routes.
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
231pub struct OutputModeParityReportV1 {
232    /// Per-command mode support entries.
233    pub entries: Vec<OutputModeParityEntryV1>,
234    /// Commands missing one or more required modes.
235    pub missing_mode_commands: Vec<String>,
236    /// Whether parity is complete.
237    pub parity_complete: bool,
238}
239
240/// Evaluate output mode parity across commands.
241pub fn evaluate_output_mode_parity(
242    entries: Vec<OutputModeParityEntryV1>,
243) -> OutputModeParityReportV1 {
244    let required_modes = ["human", "json", "jsonl", "artifact-output"];
245    let mut ordered_entries = entries;
246    ordered_entries.sort_by(|left, right| left.command.cmp(&right.command));
247    let mut missing = Vec::new();
248    for entry in &ordered_entries {
249        let mut modes = entry.supported_modes.clone();
250        modes.sort();
251        modes.dedup();
252        let has_all_required =
253            required_modes.iter().all(|mode| modes.iter().any(|value| value == mode));
254        if !has_all_required {
255            missing.push(entry.command.clone());
256        }
257    }
258    OutputModeParityReportV1 {
259        entries: ordered_entries,
260        missing_mode_commands: missing.clone(),
261        parity_complete: missing.is_empty(),
262    }
263}
264
265/// Install diagnostic component status entry.
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
267pub struct InstallDiagnosticComponentV1 {
268    /// Component key (`binary_path`, `path_resolution`, `python_bridge`, ...).
269    pub component: String,
270    /// Component health status.
271    pub healthy: bool,
272    /// Short detail for operator remediation.
273    pub detail: String,
274}
275
276/// Root install diagnosis bundle across runtime-critical components.
277#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
278pub struct InstallDiagnosisBundleV1 {
279    /// Evaluated components.
280    pub components: Vec<InstallDiagnosticComponentV1>,
281    /// Failed components list.
282    pub failing_components: Vec<String>,
283    /// Healthy summary marker.
284    pub healthy_install: bool,
285}
286
287/// Build one actionable install diagnosis bundle.
288pub fn build_install_diagnosis_bundle(
289    components: Vec<InstallDiagnosticComponentV1>,
290) -> Result<InstallDiagnosisBundleV1, String> {
291    if components.is_empty() {
292        return Err("components cannot be empty".to_string());
293    }
294    let mut ordered_components = components;
295    ordered_components.sort_by(|left, right| left.component.cmp(&right.component));
296    let failing_components: Vec<String> = ordered_components
297        .iter()
298        .filter(|entry| !entry.healthy)
299        .map(|entry| entry.component.clone())
300        .collect();
301    Ok(InstallDiagnosisBundleV1 {
302        components: ordered_components,
303        failing_components: failing_components.clone(),
304        healthy_install: failing_components.is_empty(),
305    })
306}
307
308/// Route registry entry used for completion generation.
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
310pub struct CompletionRouteEntryV1 {
311    /// Command string.
312    pub command: String,
313    /// Whether command is hidden from normal listing.
314    pub hidden: bool,
315    /// Whether command is deprecated.
316    pub deprecated: bool,
317    /// Whether command is stale and no longer routed.
318    pub stale: bool,
319}
320
321/// Generated completion snapshot from registry routes.
322#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
323pub struct CompletionSnapshotV1 {
324    /// Shell identifier (`bash`, `zsh`, `fish`).
325    pub shell: String,
326    /// Commands included in completion output.
327    pub commands: Vec<String>,
328}
329
330/// Build completion snapshot from route registry with stale/deprecated filtering.
331pub fn build_completion_snapshot_from_registry(
332    shell: &str,
333    entries: Vec<CompletionRouteEntryV1>,
334    include_deprecated: bool,
335) -> Result<CompletionSnapshotV1, String> {
336    if shell.trim().is_empty() {
337        return Err("shell cannot be empty".to_string());
338    }
339    let mut commands: Vec<String> = entries
340        .into_iter()
341        .filter(|entry| !entry.hidden)
342        .filter(|entry| !entry.stale)
343        .filter(|entry| include_deprecated || !entry.deprecated)
344        .map(|entry| entry.command)
345        .collect();
346    commands.sort();
347    commands.dedup();
348    Ok(CompletionSnapshotV1 { shell: shell.to_string(), commands })
349}
350
351/// Command side-effect classes used for preview and explain surfaces.
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
353#[serde(rename_all = "kebab-case")]
354pub enum CommandSideEffectClassV1 {
355    ReadOnly,
356    WritesConfig,
357    WritesRun,
358    ExecutesAdapter,
359    Destructive,
360}
361
362/// Side-effect preview record for a command dispatch.
363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
364pub struct CommandSideEffectPreviewV1 {
365    /// Command being evaluated.
366    pub command: String,
367    /// Classified side-effect class.
368    pub side_effect_class: CommandSideEffectClassV1,
369    /// Whether confirmation should be required before dispatch.
370    pub requires_confirmation: bool,
371}
372
373/// Classify command side-effects for dispatch safety previews.
374pub fn classify_command_side_effect(command: &str) -> Result<CommandSideEffectPreviewV1, String> {
375    if command.trim().is_empty() {
376        return Err("command cannot be empty".to_string());
377    }
378    let normalized = command.trim().to_ascii_lowercase();
379    let class = if normalized.contains("wipe") || normalized.contains("delete") {
380        CommandSideEffectClassV1::Destructive
381    } else if normalized.contains("plugins install")
382        || normalized.contains("plugins uninstall")
383        || normalized.contains("dag run")
384    {
385        CommandSideEffectClassV1::WritesRun
386    } else if normalized.contains("config set") || normalized.contains("config unset") {
387        CommandSideEffectClassV1::WritesConfig
388    } else if normalized.contains("adapter") || normalized.contains("exec") {
389        CommandSideEffectClassV1::ExecutesAdapter
390    } else {
391        CommandSideEffectClassV1::ReadOnly
392    };
393    let requires_confirmation = matches!(
394        class,
395        CommandSideEffectClassV1::WritesRun
396            | CommandSideEffectClassV1::ExecutesAdapter
397            | CommandSideEffectClassV1::Destructive
398    );
399    Ok(CommandSideEffectPreviewV1 {
400        command: command.to_string(),
401        side_effect_class: class,
402        requires_confirmation,
403    })
404}
405
406/// Rust/Python bridge parity entry for one command.
407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
408pub struct PythonBridgeParityEntryV1 {
409    /// Command under comparison.
410    pub command: String,
411    /// Rust machine envelope payload.
412    pub rust_machine_output: String,
413    /// Python bridge machine envelope payload.
414    pub python_machine_output: String,
415}
416
417/// Parity report for Rust runtime versus Python bridge command envelopes.
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
419pub struct PythonBridgeParityReportV1 {
420    /// Compared command entries.
421    pub entries: Vec<PythonBridgeParityEntryV1>,
422    /// Commands with output mismatches.
423    pub mismatched_commands: Vec<String>,
424    /// Whether parity is exact across all compared commands.
425    pub parity_exact: bool,
426}
427
428/// Build Python bridge parity report from command matrix outputs.
429pub fn build_python_bridge_command_parity_report(
430    entries: Vec<PythonBridgeParityEntryV1>,
431) -> PythonBridgeParityReportV1 {
432    let mut ordered_entries = entries;
433    ordered_entries.sort_by(|left, right| left.command.cmp(&right.command));
434    let mismatched_commands: Vec<String> = ordered_entries
435        .iter()
436        .filter(|entry| entry.rust_machine_output != entry.python_machine_output)
437        .map(|entry| entry.command.clone())
438        .collect();
439    PythonBridgeParityReportV1 {
440        entries: ordered_entries,
441        mismatched_commands: mismatched_commands.clone(),
442        parity_exact: mismatched_commands.is_empty(),
443    }
444}
445
446/// Official app route descriptor used during discovery and conflict resolution.
447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
448pub struct OfficialAppRouteDescriptorV1 {
449    /// Route namespace key.
450    pub namespace: String,
451    /// Descriptor identifier/hash.
452    pub descriptor_id: String,
453    /// Priority (larger value wins).
454    pub priority: i32,
455}
456
457/// Official app discovery report with deterministic conflict outcomes.
458#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
459pub struct OfficialAppDiscoveryReportV1 {
460    /// Winning descriptor per namespace.
461    pub winners: Vec<OfficialAppRouteDescriptorV1>,
462    /// Refused plugin namespace shadow attempts.
463    pub refused_plugin_shadows: Vec<String>,
464    /// Refused PATH shim shadow attempts.
465    pub refused_path_shims: Vec<String>,
466}
467
468/// Resolve official app discovery conflicts deterministically by namespace + priority.
469pub fn build_official_app_discovery_report(
470    descriptors: Vec<OfficialAppRouteDescriptorV1>,
471    plugin_shadow_attempts: Vec<String>,
472    path_shim_attempts: Vec<String>,
473) -> OfficialAppDiscoveryReportV1 {
474    use std::collections::BTreeMap;
475
476    let mut winners_by_namespace: BTreeMap<String, OfficialAppRouteDescriptorV1> = BTreeMap::new();
477    for descriptor in descriptors {
478        match winners_by_namespace.get(&descriptor.namespace) {
479            Some(existing) if existing.priority >= descriptor.priority => {}
480            _ => {
481                winners_by_namespace.insert(descriptor.namespace.clone(), descriptor);
482            }
483        }
484    }
485    OfficialAppDiscoveryReportV1 {
486        winners: winners_by_namespace.into_values().collect(),
487        refused_plugin_shadows: plugin_shadow_attempts,
488        refused_path_shims: path_shim_attempts,
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use serde_json::json;
495
496    use super::{
497        build_actionable_error_envelope, build_command_explain_record,
498        build_compact_operator_help_entrypoint, build_completion_snapshot_from_registry,
499        build_install_diagnosis_bundle, build_official_app_discovery_report,
500        build_python_bridge_command_parity_report, build_script_stable_command_envelope,
501        classify_command_side_effect, evaluate_output_mode_parity, ActionableFailureClassV1,
502        CompletionRouteEntryV1, InstallDiagnosticComponentV1, OfficialAppRouteDescriptorV1,
503        OutputModeParityEntryV1, PythonBridgeParityEntryV1,
504    };
505
506    #[test]
507    fn compact_help_entrypoint_requires_core_operator_routes() {
508        let report = build_compact_operator_help_entrypoint(
509            &["doctor", "plugins", "dag", "config"],
510            &["atlas", "genomics"],
511            &["community-tools"],
512            &["bijux dag --help", "bijux doctor"],
513        )
514        .expect("compact help should build");
515        assert_eq!(report.built_in_routes, vec!["config", "dag", "doctor", "plugins"]);
516        assert_eq!(report.mounted_app_routes, vec!["atlas", "genomics"]);
517    }
518
519    #[test]
520    fn script_stable_command_envelope_uses_required_machine_fields() {
521        let envelope = build_script_stable_command_envelope(
522            "command-envelope-v1",
523            "bijux dag plan",
524            true,
525            "ok",
526            json!({"plan_id":"plan-001"}),
527            vec!["using cached descriptor".to_string()],
528            Vec::new(),
529        )
530        .expect("script-stable envelope should build");
531        assert_eq!(envelope.schema_version, "command-envelope-v1");
532        assert!(envelope.ok);
533        assert_eq!(envelope.errors.len(), 0);
534        assert_eq!(envelope.warnings.len(), 1);
535    }
536
537    #[test]
538    fn actionable_error_envelope_includes_remediation_and_evidence_pointer() {
539        let error = build_actionable_error_envelope(
540            ActionableFailureClassV1::Plugin,
541            "plugin_manifest_invalid",
542            "plugin manifest rejected",
543            "run `bijux plugins inspect <name>` and correct manifest fields",
544            Some("artifacts/cli/errors/plugin-manifest-invalid.log"),
545        )
546        .expect("actionable error should build");
547        assert_eq!(error.code, "plugin_manifest_invalid");
548        assert_eq!(
549            error.evidence_pointer.as_deref(),
550            Some("artifacts/cli/errors/plugin-manifest-invalid.log")
551        );
552    }
553
554    #[test]
555    fn command_explain_includes_route_and_side_effect_contract() {
556        let record = build_command_explain_record(
557            "bijux dag run",
558            "dag.runtime.run",
559            "official-app",
560            "dag-run-envelope-v1",
561            "writes-run",
562            &["dag.run_root", "dag.cache_root"],
563        )
564        .expect("explain contract should build");
565        assert_eq!(record.route_target, "dag.runtime.run");
566        assert_eq!(record.handler_source, "official-app");
567        assert_eq!(record.required_config_keys, vec!["dag.cache_root", "dag.run_root"]);
568    }
569
570    #[test]
571    fn output_mode_parity_reports_commands_missing_required_modes() {
572        let report = evaluate_output_mode_parity(vec![
573            OutputModeParityEntryV1 {
574                command: "bijux dag run".to_string(),
575                supported_modes: vec![
576                    "human".to_string(),
577                    "json".to_string(),
578                    "jsonl".to_string(),
579                    "artifact-output".to_string(),
580                ],
581            },
582            OutputModeParityEntryV1 {
583                command: "bijux doctor".to_string(),
584                supported_modes: vec!["human".to_string(), "json".to_string()],
585            },
586        ]);
587        assert!(!report.parity_complete);
588        assert_eq!(report.missing_mode_commands, vec!["bijux doctor"]);
589    }
590
591    #[test]
592    fn install_diagnostics_bundle_identifies_failing_component() {
593        let bundle = build_install_diagnosis_bundle(vec![
594            InstallDiagnosticComponentV1 {
595                component: "binary_path".to_string(),
596                healthy: true,
597                detail: "resolved /usr/local/bin/bijux".to_string(),
598            },
599            InstallDiagnosticComponentV1 {
600                component: "python_bridge".to_string(),
601                healthy: false,
602                detail: "missing importable python bridge package".to_string(),
603            },
604        ])
605        .expect("diagnosis bundle should build");
606        assert!(!bundle.healthy_install);
607        assert_eq!(bundle.failing_components, vec!["python_bridge"]);
608    }
609
610    #[test]
611    fn completion_snapshot_excludes_hidden_stale_and_deprecated_routes() {
612        let snapshot = build_completion_snapshot_from_registry(
613            "zsh",
614            vec![
615                CompletionRouteEntryV1 {
616                    command: "bijux dag run".to_string(),
617                    hidden: false,
618                    deprecated: false,
619                    stale: false,
620                },
621                CompletionRouteEntryV1 {
622                    command: "bijux dag old-run".to_string(),
623                    hidden: false,
624                    deprecated: true,
625                    stale: false,
626                },
627                CompletionRouteEntryV1 {
628                    command: "bijux secret route".to_string(),
629                    hidden: true,
630                    deprecated: false,
631                    stale: false,
632                },
633            ],
634            false,
635        )
636        .expect("completion snapshot should build");
637        assert_eq!(snapshot.commands, vec!["bijux dag run"]);
638    }
639
640    #[test]
641    fn side_effect_classification_marks_risky_dispatches() {
642        let preview =
643            classify_command_side_effect("bijux dag run --graph sample.json").expect("classify");
644        assert!(preview.requires_confirmation);
645        assert_eq!(preview.command, "bijux dag run --graph sample.json");
646    }
647
648    #[test]
649    fn python_bridge_parity_report_detects_machine_output_drift() {
650        let report = build_python_bridge_command_parity_report(vec![
651            PythonBridgeParityEntryV1 {
652                command: "bijux status --format json".to_string(),
653                rust_machine_output: "{\"ok\":true}".to_string(),
654                python_machine_output: "{\"ok\":true}".to_string(),
655            },
656            PythonBridgeParityEntryV1 {
657                command: "bijux doctor --format json".to_string(),
658                rust_machine_output: "{\"ok\":false,\"code\":\"doctor_warn\"}".to_string(),
659                python_machine_output: "{\"ok\":false,\"code\":\"doctor_warning\"}".to_string(),
660            },
661        ]);
662        assert!(!report.parity_exact);
663        assert_eq!(report.mismatched_commands, vec!["bijux doctor --format json"]);
664    }
665
666    #[test]
667    fn official_app_discovery_prefers_highest_priority_and_refuses_shadow_attempts() {
668        let report = build_official_app_discovery_report(
669            vec![
670                OfficialAppRouteDescriptorV1 {
671                    namespace: "dag".to_string(),
672                    descriptor_id: "dag-v1".to_string(),
673                    priority: 10,
674                },
675                OfficialAppRouteDescriptorV1 {
676                    namespace: "dag".to_string(),
677                    descriptor_id: "dag-v2".to_string(),
678                    priority: 20,
679                },
680            ],
681            vec!["plugin:community attempted dag".to_string()],
682            vec!["shim:bijux-dag attempted dag".to_string()],
683        );
684        assert_eq!(report.winners.len(), 1);
685        assert_eq!(report.winners[0].descriptor_id, "dag-v2");
686        assert_eq!(report.refused_plugin_shadows, vec!["plugin:community attempted dag"]);
687    }
688}