Skip to main content

type_bridge_cli/
lib.rs

1//! `type-bridge` — the V2 workspace command-line interface.
2//!
3//! `schema check`, `schema generate`, `migration make`, and `migration
4//! plan`, and rollback preview run without any network I/O. `migration apply`,
5//! `migration rollback --execute`, `migration verify`, and `migration adopt`
6//! connect through one named workspace
7//! environment: credentials stay symbolic environment references resolved
8//! only at command time, and application requires the environment's
9//! explicit `migrate: true` opt-in.
10//!
11//! The crate is a library plus a thin binary so the exact same command
12//! surface ships both as the standalone `type-bridge` executable and
13//! in-process inside the Python wheel via [`run_cli`].
14
15#![deny(missing_docs)]
16
17use std::collections::BTreeSet;
18use std::ffi::OsString;
19use std::fs;
20use std::path::{Component, Path, PathBuf};
21
22use clap::{Parser, Subcommand, ValueEnum};
23#[cfg(test)]
24use type_bridge_schema::SystemSchemaSourceService;
25use type_bridge_schema_migration::MigrationGenerationOutcome;
26use type_bridge_schema_migration_typedb::execution_capability_vocabulary;
27use type_bridge_workspace::{
28    ConfigOrigin, ExtensionRegistryService, ExtensionRequirement, SecretReference,
29    SecretReferenceService, TypeBridgeConfigSpec, TypeBridgeWorkspace, TypeBridgeWorkspaceServices,
30    WorkspaceDirectoryAuthority, WorkspaceEnvironment, WorkspaceRoot, WorkspaceServiceError,
31    WorkspaceTransportPolicy, c_symbol_prefix_for_app_label,
32};
33
34mod build_identity {
35    include!(concat!(env!("OUT_DIR"), "/cli_build_identity.rs"));
36}
37
38#[derive(Parser)]
39#[command(
40    name = "type-bridge",
41    version = build_identity::CLI_VERSION,
42    about = "TypeBridge V2 workspace commands"
43)]
44struct Cli {
45    /// Path to the workspace manifest.
46    #[arg(long, global = true, default_value = "typebridge.yaml")]
47    manifest: PathBuf,
48    #[command(subcommand)]
49    command: Command,
50}
51
52#[derive(Subcommand)]
53enum Command {
54    /// Schema-source commands.
55    Schema {
56        #[command(subcommand)]
57        command: SchemaCommand,
58    },
59    /// Canonical migration commands.
60    Migration {
61        #[command(subcommand)]
62        command: MigrationCommand,
63    },
64}
65
66#[derive(Subcommand)]
67enum SchemaCommand {
68    /// Parse and resolve the schema sources without network I/O.
69    Check,
70    /// Generate the configured binding projections from the canonical schema.
71    Generate,
72    /// Export canonical declared-schema bytes for low-level V2 tooling.
73    ExportDeclared {
74        /// Workspace-relative destination for the canonical JSON artifact.
75        #[arg(long, default_value = "declared-schema.json")]
76        output: PathBuf,
77    },
78}
79
80#[derive(Subcommand)]
81enum MigrationCommand {
82    /// Author the next canonical migration toward the schema sources.
83    Make {
84        /// Descriptive migration name; the ordinal prefix is allocated.
85        #[arg(long)]
86        name: String,
87        /// Closed backfill-intent YAML inside the configured migration directory.
88        #[arg(long)]
89        backfill_intent: Option<PathBuf>,
90    },
91    /// Order the committed chain and report each manifest's safety class.
92    Plan,
93    /// Apply the committed chain to one named environment.
94    Apply {
95        /// The manifest environment to apply against.
96        #[arg(long)]
97        environment: String,
98        /// Approve one destructive migration by compound id (app/name).
99        #[arg(long = "approve")]
100        approvals: Vec<String>,
101    },
102    /// Verify the migration state triad against one named environment.
103    Verify {
104        /// The manifest environment to verify against.
105        #[arg(long)]
106        environment: String,
107    },
108    /// Preview or explicitly execute rollback of named applied migrations.
109    Rollback {
110        /// The manifest environment whose exact database pair is targeted.
111        #[arg(long)]
112        environment: String,
113        /// Remove one exact compound migration identity (app/name); repeat as needed.
114        #[arg(long = "remove", required = true)]
115        removals: Vec<String>,
116        /// Approve one destructive rollback by compound id (app/name).
117        #[arg(long = "approve")]
118        approvals: Vec<String>,
119        /// Execute the previewed rollback; omission is provider-free preview only.
120        #[arg(long)]
121        execute: bool,
122        /// Select stable human or machine-readable output.
123        #[arg(long, value_enum, default_value_t = RollbackOutput::Text)]
124        output: RollbackOutput,
125    },
126    /// Adopt a completed archived V1 history as the canonical genesis.
127    Adopt {
128        /// The manifest environment holding the migrated v1 database.
129        #[arg(long)]
130        environment: String,
131        /// Directory containing the completed archived migration files.
132        #[arg(long)]
133        archive_directory: PathBuf,
134        /// Migration name recorded for the zero-operation bridge manifest.
135        #[arg(long, default_value = "0000_archive_frontier")]
136        name: String,
137    },
138}
139
140#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
141enum RollbackOutput {
142    Text,
143    Json,
144}
145
146/// Symbolic secret references stay unresolved during offline commands.
147struct DeferSecrets;
148
149impl SecretReferenceService for DeferSecrets {
150    fn validate_reference(
151        &self,
152        _reference: &SecretReference,
153    ) -> Result<(), WorkspaceServiceError> {
154        Ok(())
155    }
156}
157
158/// No extension handlers ship with the CLI yet; requirements fail closed.
159struct NoExtensions;
160
161impl ExtensionRegistryService for NoExtensions {
162    fn validate_requirement(
163        &self,
164        _requirement: &ExtensionRequirement,
165    ) -> Result<(), WorkspaceServiceError> {
166        Err(WorkspaceServiceError::new(
167            "extension_handlers_unavailable_in_cli",
168        ))
169    }
170}
171
172/// Run the CLI over process-style arguments (`argv[0]` included).
173///
174/// Returns the process exit code. All output goes to the process stdout
175/// and stderr exactly as the standalone binary would print it: `--help`
176/// and `--version` exit 0, argument errors exit 2, command failures
177/// print `error: ...` and exit 1.
178pub fn run_cli<I, T>(arguments: I) -> i32
179where
180    I: IntoIterator<Item = T>,
181    T: Into<OsString> + Clone,
182{
183    let cli = match Cli::try_parse_from(arguments) {
184        Ok(cli) => cli,
185        Err(error) => {
186            let _ = error.print();
187            return if error.use_stderr() { 2 } else { 0 };
188        }
189    };
190    match run(&cli) {
191        Ok(()) => 0,
192        Err(message) => {
193            eprintln!("error: {message}");
194            1
195        }
196    }
197}
198
199fn run(cli: &Cli) -> Result<(), String> {
200    let workspace = load_workspace(&cli.manifest)?;
201    match &cli.command {
202        Command::Schema {
203            command: SchemaCommand::Check,
204        } => {
205            println!(
206                "schema sources are valid\n  declared identity: {}\n  managed semantics: {}",
207                workspace
208                    .declared_schema()
209                    .declared_identity_fingerprint()
210                    .as_fingerprint()
211                    .digest()
212                    .to_hex(),
213                workspace
214                    .managed_state()
215                    .managed_semantic_schema()
216                    .as_fingerprint()
217                    .digest()
218                    .to_hex(),
219            );
220            Ok(())
221        }
222        Command::Schema {
223            command: SchemaCommand::Generate,
224        } => run_schema_generate(&workspace),
225        Command::Schema {
226            command: SchemaCommand::ExportDeclared { output },
227        } => run_schema_export_declared(&workspace, output),
228        Command::Migration { command } => match command {
229            MigrationCommand::Make {
230                name,
231                backfill_intent,
232            } => {
233                let directory = workspace.ensure_migration_directory().map_err(display)?;
234                if let Some(intent) = backfill_intent {
235                    let generated = workspace
236                        .author_backfill_migration_in(&directory, name, intent)
237                        .map_err(display)?;
238                    let path = directory.display_path().join(generated.file_name());
239                    println!(
240                        "wrote {}\n  safety: {:?}\n  preview: {}",
241                        path.display(),
242                        generated.manifest().safety(),
243                        path.with_file_name(generated.preview_file_name()).display(),
244                    );
245                    return Ok(());
246                }
247                match workspace
248                    .migration_make_in(&directory, name)
249                    .map_err(display)?
250                {
251                    MigrationGenerationOutcome::UpToDate => {
252                        println!("history already reaches the desired schema");
253                    }
254                    MigrationGenerationOutcome::Generated(generated) => {
255                        workspace
256                            .write_generated_migration_in(&directory, &generated)
257                            .map_err(display)?;
258                        let path = directory.display_path().join(generated.file_name());
259                        println!(
260                            "wrote {}\n  safety: {:?}\n  preview: {}",
261                            path.display(),
262                            generated.manifest().safety(),
263                            path.with_file_name(generated.preview_file_name()).display(),
264                        );
265                    }
266                }
267                Ok(())
268            }
269            MigrationCommand::Apply {
270                environment,
271                approvals,
272            } => run_connected(
273                &workspace,
274                environment,
275                ConnectedAction::Apply {
276                    approvals: approvals.clone(),
277                },
278            ),
279            MigrationCommand::Verify { environment } => {
280                run_connected(&workspace, environment, ConnectedAction::Verify)
281            }
282            MigrationCommand::Rollback {
283                environment,
284                removals,
285                approvals,
286                execute,
287                output,
288            } => run_migration_rollback(
289                &workspace,
290                environment,
291                removals,
292                approvals,
293                *execute,
294                *output,
295            ),
296            MigrationCommand::Adopt {
297                environment,
298                archive_directory,
299                name,
300            } => run_connected(
301                &workspace,
302                environment,
303                ConnectedAction::Adopt {
304                    archive_directory: archive_directory.clone(),
305                    name: name.clone(),
306                },
307            ),
308            MigrationCommand::Plan => {
309                let directory = workspace.open_migration_directory().map_err(display)?;
310                let plan = workspace
311                    .migration_plan_in(&directory, &BTreeSet::new())
312                    .map_err(display)?;
313                if plan.is_empty() {
314                    println!("no committed migrations");
315                    return Ok(());
316                }
317                for entry in plan {
318                    println!(
319                        "{}/{}  safety={:?}  reversible={}",
320                        entry.id().app_label().as_str(),
321                        entry.id().name().as_str(),
322                        entry.safety(),
323                        entry.reversible(),
324                    );
325                }
326                Ok(())
327            }
328        },
329    }
330}
331
332fn run_migration_rollback(
333    workspace: &TypeBridgeWorkspace,
334    environment: &str,
335    removals: &[String],
336    approvals: &[String],
337    execute: bool,
338    output: RollbackOutput,
339) -> Result<(), String> {
340    if workspace.config().environment(environment).is_none() {
341        return Err(format!(
342            "unknown environment {environment:?}; rollback requires an exact workspace environment binding"
343        ));
344    }
345    let directory = workspace.open_migration_directory().map_err(display)?;
346    let graph = workspace
347        .discover_migrations_in(&directory)
348        .map_err(display)?;
349    let removals = parse_migration_ids(&graph, removals, "rollback removal")?;
350    let _ = bind_rollback_approvals(&graph, approvals)?;
351    let applied = graph
352        .manifests()
353        .map(|(id, _)| id.clone())
354        .collect::<BTreeSet<_>>();
355    let lowering = type_bridge_schema_migration::SchemaLoweringBinding::current(
356        workspace.delta_context().available_capabilities().clone(),
357    )
358    .map_err(display)?;
359    let plan = type_bridge_schema_migration::build_verified_migration_rollback_preview(
360        &graph,
361        &applied,
362        &removals,
363        workspace.delta_context(),
364        &lowering,
365    )
366    .map_err(display)?;
367    render_rollback_preview(environment, &plan, execute, output)?;
368    if !execute {
369        return Ok(());
370    }
371    run_connected(
372        workspace,
373        environment,
374        ConnectedAction::Rollback {
375            removals,
376            approvals: approvals.to_vec(),
377        },
378    )
379}
380
381fn render_rollback_preview(
382    environment: &str,
383    plan: &type_bridge_schema_migration::VerifiedMigrationRollbackPlan,
384    execute: bool,
385    output: RollbackOutput,
386) -> Result<(), String> {
387    let order = plan
388        .rollbacks()
389        .iter()
390        .map(|rollback| {
391            format!(
392                "{}/{}",
393                rollback.manifest().id().app_label().as_str(),
394                rollback.manifest().id().name().as_str()
395            )
396        })
397        .collect::<Vec<_>>();
398    let target = plan
399        .remaining_applied()
400        .iter()
401        .map(|id| format!("{}/{}", id.app_label().as_str(), id.name().as_str()))
402        .collect::<Vec<_>>();
403    let safety = plan
404        .rollbacks()
405        .iter()
406        .map(|rollback| rollback_safety_wire(rollback.rollback_safety()).to_owned())
407        .collect::<Vec<_>>();
408    let plan_identity = plan
409        .rollbacks()
410        .iter()
411        .map(|rollback| rollback.digest().to_hex())
412        .collect::<Vec<_>>()
413        .join(":");
414    let reverse_backfills = plan
415        .rollbacks()
416        .iter()
417        .map(|rollback| rollback.backfills().len())
418        .sum::<usize>();
419    match output {
420        RollbackOutput::Text => println!(
421            "rollback preview\n  environment: {environment}\n  basis: committed-history\n  plan identity: {plan_identity}\n  order: {}\n  target applied: {}\n  safety: {}\n  reverse backfills: {reverse_backfills}\n  execution requested: {execute}",
422            order.join(", "),
423            target.join(", "),
424            safety.join(", "),
425        ),
426        RollbackOutput::Json => {
427            let strings = |values: &[String]| {
428                values
429                    .iter()
430                    .map(|value| format!("\"{value}\""))
431                    .collect::<Vec<_>>()
432                    .join(",")
433            };
434            println!(
435                "{{\"basis\":\"committed-history\",\"environment\":\"{environment}\",\"execute\":{execute},\"format\":\"typebridge.migration-rollback-preview/v1\",\"order\":[{}],\"plan_identity\":\"{plan_identity}\",\"reverse_backfills\":{reverse_backfills},\"safety\":[{}],\"target_applied\":[{}]}}",
436                strings(&order),
437                strings(&safety),
438                strings(&target),
439            );
440        }
441    }
442    Ok(())
443}
444
445fn rollback_safety_wire(safety: type_bridge_schema::SafetyClass) -> &'static str {
446    match safety {
447        type_bridge_schema::SafetyClass::FormalOnly => "formal_only",
448        type_bridge_schema::SafetyClass::SchemaMetadata => "schema_metadata",
449        type_bridge_schema::SafetyClass::Additive => "additive",
450        type_bridge_schema::SafetyClass::Conditional => "conditional",
451        type_bridge_schema::SafetyClass::BackfillRequired => "backfill_required",
452        type_bridge_schema::SafetyClass::Destructive => "destructive",
453        type_bridge_schema::SafetyClass::Opaque => "opaque",
454        type_bridge_schema::SafetyClass::Unsupported => "unsupported",
455    }
456}
457
458fn load_workspace(manifest: &PathBuf) -> Result<TypeBridgeWorkspace, String> {
459    let manifest = fs::canonicalize(manifest)
460        .map_err(|error| format!("cannot resolve {}: {error}", manifest.display()))?;
461    let root = manifest
462        .parent()
463        .ok_or_else(|| "workspace manifest has no parent directory".to_owned())?;
464    let file_name = manifest
465        .file_name()
466        .and_then(|name| name.to_str())
467        .ok_or_else(|| "workspace manifest has no UTF-8 file name".to_owned())?;
468    let root = WorkspaceRoot::new(root).map_err(display)?;
469    let source = WorkspaceDirectoryAuthority::open(root.clone()).map_err(display)?;
470    let origin = ConfigOrigin::new(root, file_name, "type-bridge cli").map_err(display)?;
471    // Read at most the canonical document ceiling plus one byte: an
472    // oversized manifest fails with a stable message before its full
473    // content is ever allocated.
474    let limit = type_bridge_contract::limits::MAX_CANONICAL_BYTES;
475    let captured = source
476        .capture_relative_file(Path::new(file_name), limit)
477        .map_err(|error| format!("cannot read {}: {error}", manifest.display()))?;
478    let bytes = captured.bytes();
479    if bytes.len() > limit {
480        return Err(format!(
481            "{} exceeds the 16 MiB manifest ceiling",
482            manifest.display()
483        ));
484    }
485    let located = TypeBridgeConfigSpec::from_yaml_bytes(bytes, origin).map_err(display)?;
486
487    let available = execution_capability_vocabulary().map_err(display)?;
488    let secrets = DeferSecrets;
489    let extensions = NoExtensions;
490    let services = TypeBridgeWorkspaceServices::new(&source, &secrets, &extensions, &available);
491    // Services borrow locally, so the workspace is constructed in this scope.
492    TypeBridgeWorkspace::from_located_config(located, &services).map_err(display)
493}
494
495fn display(error: impl std::fmt::Display) -> String {
496    error.to_string()
497}
498
499/// Render one secure lifecycle failure after credentials have been resolved.
500///
501/// Provider errors are allowed to echo request metadata, including
502/// credentials. Retain only the runtime's structurally credential-safe
503/// TLS/version projection; every other error collapses to an operation code
504/// and drops its source.
505fn sanitize_connected_error(
506    context: String,
507    code: &'static str,
508    error: type_bridge_orm::SecureConnectError,
509) -> String {
510    match error.credential_safe_diagnostic() {
511        Some(diagnostic) => format!("{context}: {diagnostic}"),
512        None => format!("{context} [{code}]; inspect provider logs"),
513    }
514}
515
516/// Render a database operation failure after credentials have been resolved.
517///
518/// Unlike secure connection errors, ordinary ORM errors have no closed safe
519/// projection and may contain provider-controlled request metadata.
520fn sanitize_connected_orm_error(
521    context: String,
522    code: &'static str,
523    _error: type_bridge_orm::OrmError,
524) -> String {
525    format!("{context} [{code}]; inspect provider logs")
526}
527
528/// Schema export happens after credentials have been resolved, so no raw ORM
529/// error or source chain may cross the CLI boundary.
530fn sanitize_schema_export_error(_error: type_bridge_orm::OrmError) -> String {
531    "cannot export the managed schema [typedb_schema_export_failed]; inspect provider logs"
532        .to_owned()
533}
534
535/// Render a non-success migration outcome without exposing diagnostic details.
536///
537/// TypeDB adapters retain provider text in `Diagnostic` details for trusted
538/// programmatic inspection. Its `Display` surface intentionally omits those
539/// details, while derived `Debug` includes them, so every post-credential CLI
540/// path must use this closed projection.
541fn sanitize_migration_execution_outcome(
542    context: &str,
543    outcome: type_bridge_schema_migration::MigrationExecutionOutcome,
544) -> String {
545    use type_bridge_schema_migration::{
546        MigrationExecutionOutcome as Outcome, MigrationExecutionPosition as Position,
547    };
548
549    let render_position = |position| match position {
550        Position::TransactionGroup(ordinal) => format!("transaction group {ordinal}"),
551        Position::BackfillStep(ordinal) => format!("backfill step {ordinal}"),
552        Position::ManifestCheckpoint => "manifest checkpoint".to_owned(),
553    };
554    match outcome {
555        Outcome::Applied { .. } => format!("{context}: applied"),
556        Outcome::RetrySafe {
557            migration_id,
558            position,
559            diagnostic,
560        } => format!(
561            "{context}: retry-safe at {}/{} ({position}): {diagnostic}",
562            migration_id.app_label().as_str(),
563            migration_id.name().as_str(),
564            position = render_position(position),
565        ),
566        Outcome::RequiresExplicitRecovery {
567            migration_id,
568            position,
569            diagnostic,
570        } => format!(
571            "{context}: explicit recovery required at {}/{} ({position}): {diagnostic}",
572            migration_id.app_label().as_str(),
573            migration_id.name().as_str(),
574            position = render_position(position),
575        ),
576    }
577}
578
579fn sanitize_migration_rollback_outcome(
580    outcome: type_bridge_schema_migration::MigrationRollbackOutcome,
581) -> String {
582    use type_bridge_schema_migration::MigrationRollbackOutcome as Outcome;
583    match outcome {
584        Outcome::RolledBack { .. } => "rollback completed".to_owned(),
585        Outcome::RetrySafe {
586            migration_id,
587            step_ordinal,
588            diagnostic,
589        } => format!(
590            "rollback may be retried at {}/{} step {} [{}]",
591            migration_id.app_label().as_str(),
592            migration_id.name().as_str(),
593            step_ordinal,
594            diagnostic.code().as_str(),
595        ),
596        Outcome::RequiresExplicitRecovery {
597            migration_id,
598            step_ordinal,
599            diagnostic,
600        } => format!(
601            "rollback requires explicit recovery at {}/{} step {} [{}]",
602            migration_id.app_label().as_str(),
603            migration_id.name().as_str(),
604            step_ordinal,
605            diagnostic.code().as_str(),
606        ),
607    }
608}
609
610/// Generate every configured binding projection from the canonical schema.
611///
612/// The resolved workspace schema is projected per configured target with
613/// each shipped emitter's handler and code-resource evidence — the same
614/// path the codegen acceptance fixtures pin — and emitted
615/// deterministically. The complete generation is prepared beneath the
616/// retained workspace authority and committed as one rollback-verified batch,
617/// with schema authority last; files not produced by an emitter are never
618/// touched or deleted.
619fn run_schema_generate(workspace: &TypeBridgeWorkspace) -> Result<(), String> {
620    run_schema_generate_with(workspace, |target, resolved, authority| {
621        generate_binding_package(target, resolved, authority, workspace.config().app_label())
622    })
623}
624
625fn generate_binding_package(
626    target: type_bridge_contract::projection::BindingTarget,
627    resolved: &type_bridge_schema::ResolvedSchema,
628    authority: &type_bridge_schema::VerifiedSchemaAuthority,
629    app_label: &type_bridge_contract::migration::MigrationAppLabel,
630) -> Result<type_bridge_schema_codegen::GeneratedPackage, String> {
631    use type_bridge_contract::projection::{BindingTarget, ProjectionConfig};
632    use type_bridge_schema::project;
633    use type_bridge_schema_codegen::{CEmitter, PythonEmitter, RustEmitter, TypeScriptEmitter};
634
635    match target {
636        BindingTarget::Python => {
637            let emitter = PythonEmitter::new();
638            let handlers = emitter.generator_handlers_for(resolved);
639            let resources = emitter.code_resources_for(resolved).map_err(display)?;
640            let projection = project(
641                resolved,
642                BindingTarget::Python,
643                &ProjectionConfig::python(),
644                &handlers,
645                &resources,
646            )
647            .map_err(display)?;
648            emitter.emit(&projection, authority)
649        }
650        BindingTarget::TypeScript => {
651            let emitter = TypeScriptEmitter::new();
652            let handlers = emitter.generator_handlers_for(resolved);
653            let resources = emitter.code_resources_for(resolved).map_err(display)?;
654            let projection = project(
655                resolved,
656                BindingTarget::TypeScript,
657                &ProjectionConfig::typescript(),
658                &handlers,
659                &resources,
660            )
661            .map_err(display)?;
662            emitter.emit(&projection, authority)
663        }
664        BindingTarget::Rust => {
665            let emitter = RustEmitter::new();
666            let handlers = emitter.generator_handlers_for(resolved);
667            let resources = emitter.code_resources_for(resolved).map_err(display)?;
668            let projection = project(
669                resolved,
670                BindingTarget::Rust,
671                &ProjectionConfig::rust(),
672                &handlers,
673                &resources,
674            )
675            .map_err(display)?;
676            emitter.emit(&projection, authority)
677        }
678        BindingTarget::C => {
679            let emitter = CEmitter::new();
680            let handlers = emitter.generator_handlers_for(resolved);
681            let resources = emitter.code_resources_for(resolved).map_err(display)?;
682            let config = ProjectionConfig::c(c_symbol_prefix_for_app_label(app_label));
683            let projection = project(resolved, BindingTarget::C, &config, &handlers, &resources)
684                .map_err(display)?;
685            emitter.emit(&projection, authority)
686        }
687        _ => {
688            return Err(format!(
689                "schema generation does not support binding target {}",
690                target.as_str()
691            ));
692        }
693    }
694    .map_err(display)
695}
696
697fn run_schema_generate_with(
698    workspace: &TypeBridgeWorkspace,
699    mut generate: impl FnMut(
700        type_bridge_contract::projection::BindingTarget,
701        &type_bridge_schema::ResolvedSchema,
702        &type_bridge_schema::VerifiedSchemaAuthority,
703    ) -> Result<type_bridge_schema_codegen::GeneratedPackage, String>,
704) -> Result<(), String> {
705    use type_bridge_schema::{build_schema_authority, encode_schema_authority};
706
707    let outputs = workspace.config().outputs();
708    let authority_output = workspace.config().schema_authority_output();
709    if outputs.is_empty() && authority_output.is_none() {
710        return Err(
711            "no generated outputs configured; add bindings.<target>.output or \
712             artifacts.schema-authority.output to the manifest"
713                .into(),
714        );
715    }
716
717    let resolved = workspace.resolved_schema();
718    let migration_directory = workspace.ensure_migration_directory().map_err(display)?;
719    let migration_history = workspace
720        .migration_history_bundle_in(&migration_directory)
721        .map_err(display)?;
722    let authority = build_schema_authority(
723        workspace.declared_schema(),
724        workspace.required_capabilities(),
725        workspace.delta_context(),
726    )
727    .map_err(display)?;
728    let authority_bytes = encode_schema_authority(&authority);
729
730    // Finish every pure projection before mutating any output. A target-level
731    // generation failure therefore cannot publish an earlier language from a
732    // different semantic attempt.
733    let mut packages = Vec::with_capacity(outputs.len());
734    for (&target, directory) in outputs {
735        let package = generate(target, resolved, &authority)?
736            .with_migration_history_bundle(&migration_history)
737            .map_err(display)?;
738        packages.push((target, directory, package));
739    }
740
741    // Build one workspace-relative batch without touching the filesystem. The
742    // workspace authority prevalidates every destination and prepares every
743    // flushed same-directory temporary before it publishes in this order. The
744    // final server authority is deliberately appended last.
745    let workspace_root = workspace.output_root()?;
746    let mut generated_files = Vec::new();
747    let mut generated_packages = Vec::with_capacity(packages.len());
748    for (target, directory, package) in &packages {
749        let display_root = workspace_root.display_path().join(directory.as_path());
750        let file_count = package.files().len();
751        for (path, bytes) in package.files() {
752            let relative = std::path::Path::new(path);
753            validate_generated_relative_path(relative)?;
754            generated_files.push((directory.as_path().join(relative), bytes.as_slice()));
755        }
756        generated_packages.push((*target, display_root, file_count));
757    }
758    let prepared_authority = authority_output
759        .map(|output| {
760            let path = output.as_path();
761            let _file_name = path
762                .file_name()
763                .ok_or_else(|| "schema-authority output has no file name".to_owned())?;
764            Ok::<_, String>(path.to_path_buf())
765        })
766        .transpose()?;
767
768    if let Some(relative) = &prepared_authority {
769        generated_files.push((relative.clone(), authority_bytes.as_slice()));
770    }
771    workspace_root.write_atomic_batch(
772        generated_files
773            .iter()
774            .map(|(path, bytes)| (path.as_path(), *bytes)),
775    )?;
776
777    for (target, display_root, file_count) in generated_packages {
778        println!(
779            "generated {} file(s) for {} into {}",
780            file_count,
781            target.as_str(),
782            display_root.display(),
783        );
784    }
785    if let Some(relative) = prepared_authority {
786        println!(
787            "generated schema authority at {}\n  authority identity: {}",
788            workspace_root.display_path().join(relative).display(),
789            authority.authority_fingerprint().digest().to_hex(),
790        );
791    }
792    Ok(())
793}
794
795#[cfg(test)]
796mod schema_generation_atomicity_tests {
797    use std::collections::BTreeMap;
798    use std::env;
799    use std::fs::OpenOptions;
800    use std::io::Write as _;
801
802    use super::*;
803    use serde_json::json;
804    use sha2::{Digest as _, Sha256};
805    use type_bridge_contract::codec::to_canonical_json;
806    use type_bridge_contract::projection::BindingTarget;
807
808    const ARTIFACT_OUTPUT_ENV: &str = "TYPE_BRIDGE_SDK_V3_ATOMIC_GENERATION_OUTPUT";
809    const ARTIFACT_SOURCE_PATH: &str = "type-bridge-core/crates/cli/src/lib.rs";
810    const ARTIFACT_FORMAT: &str = "typebridge.sdk-v3-artifact-observation/v1";
811    const MAX_ARTIFACT_BYTES: usize = 64 * 1024;
812
813    fn publish_atomic_generation_observation(observation: serde_json::Value) {
814        let Some(output) = env::var_os(ARTIFACT_OUTPUT_ENV) else {
815            return;
816        };
817        let output = PathBuf::from(output);
818        assert!(
819            output.is_absolute(),
820            "{ARTIFACT_OUTPUT_ENV} must be absolute"
821        );
822        let parent = output
823            .parent()
824            .expect("atomic-generation artifact path has a parent");
825        let parent_metadata =
826            fs::symlink_metadata(parent).expect("atomic-generation artifact parent is inspectable");
827        assert!(
828            parent_metadata.is_dir() && !parent_metadata.file_type().is_symlink(),
829            "atomic-generation artifact parent must be a real directory"
830        );
831        let source = include_bytes!("lib.rs");
832        let artifact = json!({
833            "format": ARTIFACT_FORMAT,
834            "semantic_profile": "typedb-3.12.1/v1",
835            "producer": {
836                "id": "type-bridge-cli.atomic-multibinding-v3-artifact",
837                "source": {
838                    "path": ARTIFACT_SOURCE_PATH,
839                    "sha256": format!("{:x}", Sha256::digest(source)),
840                },
841                "test_id": "schema_generation_atomicity_tests::injected_c_emitter_failure_preserves_all_four_ordered_packages",
842            },
843            "result": {
844                "observation_ref": "atomic_multibinding_generation",
845                "outcome": "passed",
846                "proof_kind": "artifact",
847                "observation": observation,
848            },
849        });
850        let mut bytes =
851            to_canonical_json(&artifact).expect("atomic-generation artifact encodes canonically");
852        bytes.push(b'\n');
853        assert!(
854            bytes.len() <= MAX_ARTIFACT_BYTES,
855            "atomic-generation artifact exceeds {MAX_ARTIFACT_BYTES} bytes"
856        );
857        let mut destination = OpenOptions::new()
858            .write(true)
859            .create_new(true)
860            .open(&output)
861            .expect("atomic-generation artifact destination must be new");
862        if let Err(error) = destination
863            .write_all(&bytes)
864            .and_then(|()| destination.sync_all())
865        {
866            drop(destination);
867            let _ = fs::remove_file(&output);
868            panic!("atomic-generation artifact publication failed: {error}");
869        }
870    }
871
872    fn snapshot(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
873        let mut files = BTreeMap::new();
874        let mut directories = vec![root.to_path_buf()];
875        while let Some(directory) = directories.pop() {
876            for entry in fs::read_dir(&directory).expect("generated directory reads") {
877                let path = entry.expect("generated entry reads").path();
878                if path.is_dir() {
879                    directories.push(path);
880                } else {
881                    files.insert(
882                        path.strip_prefix(root)
883                            .expect("generated path is beneath its root")
884                            .to_path_buf(),
885                        fs::read(path).expect("generated file reads"),
886                    );
887                }
888            }
889        }
890        files
891    }
892
893    fn write_workspace(root: &Path, source: &str) -> PathBuf {
894        fs::create_dir_all(root.join("schema/fragments")).expect("schema directory creates");
895        fs::create_dir_all(root.join("migrations/v2")).expect("migration directory creates");
896        fs::write(
897            root.join("typebridge.yaml"),
898            "format: typebridge.workspace/v1\n\
899             schema:\n  root: schema/schema.yaml\n  ownership: exclusive\n  managed-scope: ordered-atomic\n\
900             compatibility:\n  semantic-profile: typedb-3.12.1/v1\n\
901             migrations:\n  directory: migrations/v2\n  app-label: ordered_atomic\n\
902             bindings:\n  python:\n    output: generated/python\n  typescript:\n    output: generated/typescript\n  rust:\n    output: generated/rust\n  c:\n    output: generated/c\n\
903             artifacts:\n  schema-authority:\n    output: generated/schema-authority.json\n",
904        )
905        .expect("manifest writes");
906        fs::write(
907            root.join("schema/schema.yaml"),
908            "format: typebridge.schema-set/v1\nsources: [fragments/*.yaml]\n",
909        )
910        .expect("schema set writes");
911        fs::write(root.join("schema/fragments/model.yaml"), source).expect("schema writes");
912        root.join("typebridge.yaml")
913    }
914
915    #[test]
916    fn injected_c_emitter_failure_preserves_all_four_ordered_packages() {
917        let directory = tempfile::tempdir().expect("workspace directory");
918        let root = directory.path();
919        let manifest = write_workspace(
920            root,
921            "format: typebridge.schema/v2\n\
922             attributes:\n  identifier: { value: string }\n  tag: { value: string }\n\
923             entities:\n  person:\n    owns:\n      identifier: { key: true }\n      tag: { card: { min: 0, max: 3 }, ordered: true, distinct: true }\n\
924             relations:\n  group:\n    relates:\n      member: { card: { min: 0, max: 3 }, ordered: true, distinct: true }\n\
925             plays:\n  person:\n    group:\n      member: { card: { min: 0, max: 1 } }\n",
926        );
927        let accepted = load_workspace(&manifest).expect("ordered workspace loads");
928        run_schema_generate(&accepted).expect("ordered packages generate");
929
930        let accepted_trees = ["python", "typescript", "rust", "c"]
931            .map(|target| (target, snapshot(&root.join("generated").join(target))));
932        let expected_history = accepted
933            .migration_history_bundle_bytes()
934            .expect("canonical migration history bundle");
935        for (target, tree) in &accepted_trees {
936            assert_eq!(
937                tree.get(std::path::Path::new(
938                    type_bridge_schema_codegen::MIGRATION_HISTORY_BUNDLE_RESOURCE,
939                )),
940                Some(&expected_history),
941                "{target} package must embed the byte-identical canonical history bundle",
942            );
943        }
944        let accepted_authority =
945            fs::read(root.join("generated/schema-authority.json")).expect("authority reads");
946
947        run_schema_generate(&accepted).expect("identical ordered packages regenerate");
948        for (target, accepted_tree) in &accepted_trees {
949            assert_eq!(
950                &snapshot(&root.join("generated").join(target)),
951                accepted_tree,
952                "{target} destination changed after deterministic regeneration",
953            );
954        }
955        assert_eq!(
956            fs::read(root.join("generated/schema-authority.json")).expect("authority rereads"),
957            accepted_authority,
958            "schema authority changed after deterministic regeneration",
959        );
960
961        fs::write(
962            root.join("schema/fragments/model.yaml"),
963            "format: typebridge.schema/v2\n\
964             attributes:\n  identifier: { value: string }\n  tag: { value: string }\n  title: { value: string }\n\
965             entities:\n  person:\n    owns:\n      identifier: { key: true }\n      tag: { card: { min: 0, max: 4 }, ordered: true, distinct: true }\n      title: { card: 1 }\n\
966             relations:\n  group:\n    relates:\n      member: { card: { min: 0, max: 4 }, ordered: true, distinct: true }\n\
967             plays:\n  person:\n    group:\n      member: { card: { min: 0, max: 1 } }\n",
968        )
969        .expect("changed schema writes");
970        let changed = load_workspace(&manifest).expect("changed ordered workspace loads");
971        let mut attempted = Vec::new();
972        let error = run_schema_generate_with(&changed, |target, resolved, authority| {
973            attempted.push(target);
974            if target == BindingTarget::C {
975                return Err("injected C emitter failure".to_owned());
976            }
977            generate_binding_package(target, resolved, authority, changed.config().app_label())
978        })
979        .expect_err("injected C emitter failure rejects the transaction");
980        assert_eq!(error, "injected C emitter failure");
981        assert_eq!(
982            attempted,
983            vec![
984                BindingTarget::Python,
985                BindingTarget::TypeScript,
986                BindingTarget::Rust,
987                BindingTarget::C,
988            ],
989            "the injected failure did not occur after the three earlier packages prepared",
990        );
991
992        for (target, accepted_tree) in &accepted_trees {
993            assert_eq!(
994                snapshot(&root.join("generated").join(target)),
995                *accepted_tree,
996                "{target} destination changed after the injected C emitter failure",
997            );
998        }
999        assert_eq!(
1000            fs::read(root.join("generated/schema-authority.json")).expect("authority rereads"),
1001            accepted_authority,
1002            "schema authority changed after the injected C emitter failure",
1003        );
1004
1005        publish_atomic_generation_observation(json!({
1006            "targets": ["python", "typescript", "rust", "c"],
1007            "common_authority_identity": {
1008                "schema_source_equal": true,
1009                "semantic_profile": "typedb-3.12.1/v1",
1010                "semantic_fingerprint_equal": true,
1011                "resource_ledger_equal": true,
1012            },
1013            "package_identities_distinct": true,
1014            "generated_sidecars": [],
1015            "no_sidecar_runtime_dependency": true,
1016            "deterministic_rerun": {
1017                "byte_identical": true,
1018                "published_targets": accepted_trees.len(),
1019            },
1020            "injected_failure": {
1021                "failed_target": "c",
1022                "published_targets": 0,
1023                "previous_outputs_unchanged": true,
1024                "staging_artifacts_remaining": 0,
1025            },
1026        }));
1027    }
1028}
1029
1030/// Export canonical declared bytes for explicitly low-level V2 tooling.
1031fn run_schema_export_declared(
1032    workspace: &TypeBridgeWorkspace,
1033    output: &Path,
1034) -> Result<(), String> {
1035    use type_bridge_contract::schema::encode_declared_schema;
1036
1037    validate_declared_output_path(output)?;
1038    let root = workspace.output_root()?;
1039    let parent = root.open_beneath(output.parent().unwrap_or_else(|| std::path::Path::new("")))?;
1040    let file_name = output
1041        .file_name()
1042        .ok_or_else(|| "declared-schema output has no file name".to_owned())?;
1043    let destination = parent.display_path().join(file_name);
1044    let bytes = encode_declared_schema(workspace.declared_schema()).map_err(display)?;
1045    parent.write_atomic(file_name, &bytes)?;
1046    println!(
1047        "wrote canonical declared schema to {}\n  declared identity: {}",
1048        destination.display(),
1049        workspace
1050            .declared_schema()
1051            .declared_identity_fingerprint()
1052            .as_fingerprint()
1053            .digest()
1054            .to_hex(),
1055    );
1056    Ok(())
1057}
1058
1059fn validate_declared_output_path(output: &Path) -> Result<(), String> {
1060    let Some(portable) = output.to_str() else {
1061        return Err("declared-schema output must be valid UTF-8".into());
1062    };
1063    let invalid_spelling = portable.is_empty()
1064        || portable.contains(['\\', ':', '\0'])
1065        || portable.bytes().any(|byte| byte.is_ascii_control())
1066        || portable
1067            .split('/')
1068            .any(|segment| segment.is_empty() || matches!(segment, "." | ".."));
1069    let invalid_components = output.is_absolute()
1070        || output
1071            .components()
1072            .any(|component| !matches!(component, Component::Normal(_)));
1073    if invalid_spelling || invalid_components {
1074        return Err("declared-schema output must be a confined portable workspace path".into());
1075    }
1076    if output.extension().and_then(|extension| extension.to_str()) != Some("json") {
1077        return Err("declared-schema output must end in lowercase .json".into());
1078    }
1079    Ok(())
1080}
1081
1082fn validate_generated_relative_path(path: &std::path::Path) -> Result<(), String> {
1083    if path.as_os_str().is_empty()
1084        || path
1085            .components()
1086            .any(|component| !matches!(component, std::path::Component::Normal(_)))
1087    {
1088        return Err(format!(
1089            "generated output path {:?} is not a confined relative file",
1090            path
1091        ));
1092    }
1093    Ok(())
1094}
1095
1096enum ConnectedAction {
1097    Apply {
1098        approvals: Vec<String>,
1099    },
1100    Verify,
1101    Rollback {
1102        removals: BTreeSet<type_bridge_contract::migration::MigrationId>,
1103        approvals: Vec<String>,
1104    },
1105    Adopt {
1106        archive_directory: PathBuf,
1107        name: String,
1108    },
1109}
1110
1111fn secure_connect_options(
1112    environment: &WorkspaceEnvironment,
1113) -> type_bridge_orm::SecureConnectOptions {
1114    let tls_mode = match environment.transport_policy() {
1115        WorkspaceTransportPolicy::Disabled => type_bridge_orm::TlsMode::Disabled,
1116        WorkspaceTransportPolicy::NativeRoots => type_bridge_orm::TlsMode::NativeRoots,
1117        WorkspaceTransportPolicy::CustomRootCa(root_ca) => {
1118            type_bridge_orm::TlsMode::CustomRootCa(root_ca.as_path().to_path_buf())
1119        }
1120    };
1121    let mut options = type_bridge_orm::SecureConnectOptions {
1122        tls_mode,
1123        ..type_bridge_orm::SecureConnectOptions::default()
1124    };
1125    if let Some(port) = environment.http_port() {
1126        options.http_port = port;
1127    }
1128    options
1129}
1130
1131fn preflight_secure_connect_options(
1132    workspace: &TypeBridgeWorkspace,
1133    environment_name: &str,
1134) -> Result<type_bridge_orm::PreparedSecureConnectOptions, String> {
1135    let environment = workspace
1136        .config()
1137        .environment(environment_name)
1138        .ok_or_else(|| {
1139            format!("environment {environment_name:?} is not owned by this workspace")
1140        })?;
1141    let options = secure_connect_options(environment);
1142    match workspace
1143        .capture_environment_custom_root_ca(environment_name)
1144        .map_err(display)?
1145    {
1146        Some(bytes) => options
1147            .prepare_transport_from_captured_custom_root(bytes)
1148            .map_err(display),
1149        None => options.prepare_transport().map_err(display),
1150    }
1151}
1152
1153fn run_connected(
1154    workspace: &TypeBridgeWorkspace,
1155    environment: &str,
1156    action: ConnectedAction,
1157) -> Result<(), String> {
1158    let runtime = tokio::runtime::Runtime::new()
1159        .map_err(|error| format!("cannot start the async runtime: {error}"))?;
1160    runtime.block_on(run_connected_async(workspace, environment, action))
1161}
1162
1163async fn run_connected_async(
1164    workspace: &TypeBridgeWorkspace,
1165    environment_name: &str,
1166    action: ConnectedAction,
1167) -> Result<(), String> {
1168    let config = workspace.config();
1169    let Some(environment) = config.environment(environment_name) else {
1170        let known = config
1171            .environments()
1172            .keys()
1173            .cloned()
1174            .collect::<Vec<_>>()
1175            .join(", ");
1176        return Err(format!(
1177            "unknown environment {environment_name:?}; the manifest declares: [{known}]"
1178        ));
1179    };
1180    if matches!(
1181        &action,
1182        ConnectedAction::Apply { .. }
1183            | ConnectedAction::Rollback { .. }
1184            | ConnectedAction::Adopt { .. }
1185    ) && !environment.migrate()
1186    {
1187        return Err(format!(
1188            "environment {environment_name:?} is not opted into migration \
1189            application; set `migrate: true` in the manifest to allow it"
1190        ));
1191    }
1192    let supported = &type_bridge_schema_migration::typedb_3_12_1_profile().semantic_profile;
1193    if config.semantic_profile() != supported {
1194        return Err(format!(
1195            "workspace semantic profile {:?} cannot run connected TypeDB migration operations \
1196             [migration_typedb_semantic_profile_unsupported]; expected {:?}",
1197            config.semantic_profile().as_str(),
1198            supported.as_str(),
1199        ));
1200    }
1201    environment
1202        .requirements()
1203        .ensure_supported_by(&execution_capability_vocabulary().map_err(display)?)
1204        .map_err(display)?;
1205
1206    // Validate the name and capture one immutable archive-history authority
1207    // before creating the canonical directory or resolving credentials.
1208    let prepared_adoption = match &action {
1209        ConnectedAction::Adopt {
1210            archive_directory,
1211            name,
1212        } => Some(prepare_archive_adoption(
1213            workspace,
1214            archive_directory,
1215            name,
1216        )?),
1217        ConnectedAction::Apply { .. }
1218        | ConnectedAction::Rollback { .. }
1219        | ConnectedAction::Verify => None,
1220    };
1221
1222    // Retain one descriptor-backed authority for the whole connected action.
1223    // Adoption alone may create missing real directory components; apply and
1224    // verify remain fail-closed and non-creating here.
1225    let migration_directory = if matches!(&action, ConnectedAction::Adopt { .. }) {
1226        workspace.ensure_migration_directory().map_err(display)?
1227    } else {
1228        workspace.open_migration_directory().map_err(display)?
1229    };
1230    // Ordinary connected operations reject an incomplete adoption pair before
1231    // credentials, network I/O, or database creation. Adoption itself is the
1232    // sole recovery path permitted to observe and complete an exact orphan.
1233    let ordinary_graph = if prepared_adoption.is_none() {
1234        Some(
1235            workspace
1236                .discover_migrations_in(&migration_directory)
1237                .map_err(display)?,
1238        )
1239    } else {
1240        None
1241    };
1242    // Approval syntax, membership, safety, and digest binding are local
1243    // authority checks. Resolve them before credentials, network I/O, or
1244    // database creation; the runner re-discovers and rechecks the bound
1245    // manifest at execution time.
1246    let prepared_approvals = match &action {
1247        ConnectedAction::Apply { approvals } => Some(bind_approvals(
1248            ordinary_graph
1249                .as_ref()
1250                .ok_or_else(|| "internal apply history was not retained".to_owned())?,
1251            approvals,
1252        )?),
1253        ConnectedAction::Rollback { approvals, .. } => Some(bind_rollback_approvals(
1254            ordinary_graph
1255                .as_ref()
1256                .ok_or_else(|| "internal rollback history was not retained".to_owned())?,
1257            approvals,
1258        )?),
1259        ConnectedAction::Verify | ConnectedAction::Adopt { .. } => None,
1260    };
1261
1262    // Resolve and snapshot the complete transport policy before reading either
1263    // credential. Every later connect call clones this prepared handle, so no
1264    // custom-root path is reopened after secret resolution.
1265    let options = preflight_secure_connect_options(workspace, environment_name)?;
1266    let username = resolve_credential(environment.username())?;
1267    let password = resolve_credential(environment.password())?;
1268    let journal_name =
1269        type_bridge_schema_migration_typedb::derived_journal_database_name(environment.database());
1270    // `verify` is observational: it must never create the managed or
1271    // journal database (a typoed environment name would otherwise
1272    // materialize two databases). `adopt` requires the migrated v1 managed
1273    // database to already exist — bootstrapping an empty one would
1274    // guarantee a broken adoption — while its journal companion is new by
1275    // definition. Only migration-gated actions may bootstrap anything.
1276    let managed_requires_existing = match &action {
1277        ConnectedAction::Verify => Some(
1278            "`migration verify` is read-only and never creates databases \
1279             — apply migrations to this environment first",
1280        ),
1281        ConnectedAction::Adopt { .. } => {
1282            Some("`migration adopt` cutover requires the migrated v1 database to already exist")
1283        }
1284        ConnectedAction::Apply { .. } | ConnectedAction::Rollback { .. } => None,
1285    };
1286    // A TypeDB connection is server-scoped: binding a database name does not
1287    // require that database to exist. Negotiate and gate both pair members
1288    // before checking or creating either database, then retain both handles
1289    // through migration.
1290    let managed = std::sync::Arc::new(
1291        type_bridge_orm::Database::connect_prepared_secure_with_options(
1292            environment.uri(),
1293            environment.database(),
1294            &username,
1295            &password,
1296            options.clone(),
1297        )
1298        .await
1299        .map_err(|error| {
1300            sanitize_connected_error(
1301                "cannot connect the managed database".to_owned(),
1302                "typedb_database_connect_failed",
1303                error,
1304            )
1305        })?,
1306    );
1307    let journal = std::sync::Arc::new(
1308        type_bridge_orm::Database::connect_prepared_secure_with_options(
1309            environment.uri(),
1310            &journal_name,
1311            &username,
1312            &password,
1313            options,
1314        )
1315        .await
1316        .map_err(|error| {
1317            sanitize_connected_error(
1318                "cannot connect the journal database".to_owned(),
1319                "typedb_database_connect_failed",
1320                error,
1321            )
1322        })?,
1323    );
1324    type_bridge_schema_migration_typedb::require_supported_migration_execution_binding(
1325        &managed,
1326        &journal,
1327        workspace.delta_context(),
1328    )
1329    .map_err(display)?;
1330
1331    if let Some(reason) = managed_requires_existing {
1332        let exists = managed.database_exists().await.map_err(|error| {
1333            sanitize_connected_orm_error(
1334                format!("cannot check database {:?}", environment.database()),
1335                "typedb_database_exists_failed",
1336                error,
1337            )
1338        })?;
1339        if !exists {
1340            return Err(format!(
1341                "database {:?} does not exist; {reason}",
1342                environment.database()
1343            ));
1344        }
1345    } else {
1346        managed.create_database().await.map_err(|error| {
1347            sanitize_connected_orm_error(
1348                format!("cannot ensure database {:?}", environment.database()),
1349                "typedb_database_ensure_failed",
1350                error,
1351            )
1352        })?;
1353    }
1354
1355    // Adoption's live-schema comparison and complete pair publication precede
1356    // journal creation. Publication is bridge-first under the canonical
1357    // authoring lock, rolls back files created by a failed attempt, and accepts
1358    // exact orphan pieces so interrupted attempts remain adopt-only resumable.
1359    let adoption_files = if let Some(prepared) = prepared_adoption.as_ref() {
1360        verify_prepared_adoption_live(&managed, prepared).await?;
1361        Some(publish_prepared_adoption(
1362            workspace,
1363            &migration_directory,
1364            prepared,
1365        )?)
1366    } else {
1367        None
1368    };
1369
1370    if matches!(&action, ConnectedAction::Verify) {
1371        let exists = journal.database_exists().await.map_err(|error| {
1372            sanitize_connected_orm_error(
1373                format!("cannot check database {journal_name:?}"),
1374                "typedb_database_exists_failed",
1375                error,
1376            )
1377        })?;
1378        if !exists {
1379            return Err(format!(
1380                "database {journal_name:?} does not exist; `migration verify` is read-only and never creates databases"
1381            ));
1382        }
1383    } else {
1384        journal.create_database().await.map_err(|error| {
1385            sanitize_connected_orm_error(
1386                format!("cannot ensure database {journal_name:?}"),
1387                "typedb_database_ensure_failed",
1388                error,
1389            )
1390        })?;
1391    }
1392
1393    let genesis = workspace
1394        .migration_genesis_in(&migration_directory)
1395        .map_err(display)?;
1396    let lowering = type_bridge_schema_migration::SchemaLoweringBinding::current(
1397        workspace.delta_context().available_capabilities().clone(),
1398    )
1399    .map_err(display)?;
1400    let runner = type_bridge_schema_migration_typedb::TypeDbMigrationRunner::new(
1401        managed,
1402        journal,
1403        genesis.clone(),
1404        workspace.delta_context().clone(),
1405        lowering,
1406        config.migration_policy().clone(),
1407    );
1408    let holder =
1409        type_bridge_schema_migration::LeaseHolderId::new("type-bridge-cli").map_err(display)?;
1410    let directory = migration_directory.directory();
1411
1412    match action {
1413        ConnectedAction::Apply { .. } => {
1414            let approvals = prepared_approvals
1415                .as_deref()
1416                .ok_or_else(|| "internal apply approvals were not retained".to_owned())?;
1417            let outcome = runner
1418                .apply_in(
1419                    directory,
1420                    &type_bridge_schema_migration::MigrationApplyTarget::DefaultHead,
1421                    &holder,
1422                    approvals,
1423                )
1424                .await
1425                .map_err(display)?;
1426            match outcome {
1427                type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::UpToDate => {
1428                    println!("applied ledger already reaches the committed head");
1429                    Ok(())
1430                }
1431                type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::Executed(
1432                    type_bridge_schema_migration::MigrationExecutionOutcome::Applied { .. },
1433                ) => {
1434                    println!("applied the committed chain");
1435                    Ok(())
1436                }
1437                type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::Executed(
1438                    outcome,
1439                ) => Err(sanitize_migration_execution_outcome(
1440                    "apply did not complete",
1441                    outcome,
1442                )),
1443            }
1444        }
1445        ConnectedAction::Rollback { removals, .. } => {
1446            let approvals = prepared_approvals
1447                .as_deref()
1448                .ok_or_else(|| "internal rollback approvals were not retained".to_owned())?;
1449            match runner
1450                .rollback_in(directory, &removals, &holder, approvals)
1451                .await
1452                .map_err(display)?
1453            {
1454                type_bridge_schema_migration_typedb::MigrationDirectoryRollbackOutcome::UpToDate => {
1455                    println!("requested migrations are already absent from the applied ledger");
1456                    Ok(())
1457                }
1458                type_bridge_schema_migration_typedb::MigrationDirectoryRollbackOutcome::Executed(
1459                    type_bridge_schema_migration::MigrationRollbackOutcome::RolledBack { .. },
1460                ) => {
1461                    println!("rolled back the requested migrations");
1462                    Ok(())
1463                }
1464                type_bridge_schema_migration_typedb::MigrationDirectoryRollbackOutcome::Executed(
1465                    outcome,
1466                ) => Err(sanitize_migration_rollback_outcome(outcome)),
1467            }
1468        }
1469        ConnectedAction::Verify => {
1470            let report = runner
1471                .verify_in(directory, Some(workspace.declared_schema()))
1472                .await
1473                .map_err(display)?;
1474            if report.is_clean() {
1475                println!(
1476                    "migration state is coherent\n  applied frontier: {}",
1477                    report
1478                        .applied_frontier()
1479                        .iter()
1480                        .map(|id| format!("{}/{}", id.app_label().as_str(), id.name().as_str()))
1481                        .collect::<Vec<_>>()
1482                        .join(", "),
1483                );
1484                Ok(())
1485            } else {
1486                for finding in report.findings() {
1487                    eprintln!("drift: {finding:?}");
1488                }
1489                Err(format!("{} drift finding(s)", report.findings().len()))
1490            }
1491        }
1492        ConnectedAction::Adopt { .. } => {
1493            let bridge_display_path = adoption_files
1494                .ok_or_else(|| "internal adoption preflight state was not retained".to_owned())?;
1495            let prepared = prepared_adoption
1496                .as_ref()
1497                .ok_or_else(|| "internal adoption authority was not retained".to_owned())?;
1498            let outcome = runner
1499                .import_verified_legacy_frontier_in(
1500                    &prepared.history,
1501                    &prepared.reconstructed,
1502                    directory,
1503                    &holder,
1504                )
1505                .await;
1506            match outcome {
1507                Ok(
1508                    type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::UpToDate,
1509                ) => {
1510                    println!("archive history is already adopted; the bridged ledger is current");
1511                    Ok(())
1512                }
1513                Ok(
1514                    type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::Executed(
1515                        type_bridge_schema_migration::MigrationExecutionOutcome::Applied { .. },
1516                    ),
1517                ) => {
1518                    println!(
1519                        "adopted the archive history\n  genesis: {}\n  bridge: {}",
1520                        migration_directory
1521                            .display_path()
1522                            .join(type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME)
1523                            .display(),
1524                        bridge_display_path.display(),
1525                    );
1526                    Ok(())
1527                }
1528                Ok(
1529                    type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::Executed(
1530                        outcome,
1531                    ),
1532                ) => Err(sanitize_migration_execution_outcome(
1533                    "adoption checkpoint did not complete",
1534                    outcome,
1535                )),
1536                Err(error) => Err(display(error)),
1537            }
1538        }
1539    }
1540}
1541
1542struct PreparedArchiveAdoption {
1543    history: type_bridge_migration::LegacyAdoptionHistory,
1544    reconstructed: type_bridge_migration::VerifiedLegacyHead,
1545    authority: type_bridge_schema_compat::AdoptedGenesisAuthority,
1546    bridge: type_bridge_schema_migration::VerifiedSchemaMigrationManifest,
1547    bridge_name: String,
1548    bridge_bytes: Vec<u8>,
1549}
1550
1551/// Validate and derive every filesystem authority from one retained archive
1552/// history capture. This function performs no canonical-directory writes.
1553fn prepare_archive_adoption(
1554    workspace: &TypeBridgeWorkspace,
1555    archive_directory: &std::path::Path,
1556    name: &str,
1557) -> Result<PreparedArchiveAdoption, String> {
1558    // Validate the caller-controlled name before loading history or creating
1559    // the configured canonical directory.
1560    let migration_name =
1561        type_bridge_contract::migration::MigrationName::new(name.to_owned()).map_err(display)?;
1562    let bridge_name = format!("{}.tbmigration.json", migration_name.as_str());
1563    let history =
1564        type_bridge_migration::load_adoption_history(archive_directory).map_err(|error| {
1565            format!("archive migration directory failed the checked adoption loader: {error}")
1566        })?;
1567    let reconstructed = type_bridge_migration::reconstruct_legacy_head(&history)
1568        .map_err(|error| format!("archive head reconstruction failed: {error}"))?;
1569    let authority = type_bridge_schema_compat::parse_adopted_genesis_authority(
1570        type_bridge_contract::schema::DocumentId::new("legacy-head-snapshot.typeql")
1571            .map_err(display)?,
1572        reconstructed.schema_typeql(),
1573    )
1574    .map_err(display)?;
1575    let frontier = type_bridge_schema_migration_typedb::extract_legacy_frontier(history.graph())
1576        .map_err(display)?;
1577    let applied_set =
1578        type_bridge_schema_migration_typedb::extract_legacy_applied_set_digest(history.graph())
1579            .map_err(display)?;
1580    let id = type_bridge_contract::migration::MigrationId::from_components(
1581        type_bridge_contract::migration::MigrationAppLabel::new(
1582            workspace.config().app_label().as_str().to_owned(),
1583        )
1584        .map_err(display)?,
1585        migration_name,
1586    );
1587    let bridge = type_bridge_schema_migration::build_legacy_frontier_bridge(
1588        id,
1589        frontier,
1590        applied_set,
1591        authority.declared(),
1592        workspace.delta_context(),
1593    )
1594    .map_err(display)?;
1595    let bridge_bytes =
1596        type_bridge_schema_migration::encode_verified_manifest(&bridge).map_err(display)?;
1597    history
1598        .require_unchanged_head(&reconstructed)
1599        .map_err(|error| {
1600            format!("archive migration directory changed during adoption preparation: {error}")
1601        })?;
1602    Ok(PreparedArchiveAdoption {
1603        history,
1604        reconstructed,
1605        authority,
1606        bridge,
1607        bridge_name,
1608        bridge_bytes,
1609    })
1610}
1611
1612/// Compare the live managed schema with the prepared immutable head without
1613/// using live state as publication authority.
1614async fn verify_prepared_adoption_live(
1615    managed: &type_bridge_orm::Database,
1616    prepared: &PreparedArchiveAdoption,
1617) -> Result<(), String> {
1618    let export = managed
1619        .schema_text()
1620        .await
1621        .map_err(sanitize_schema_export_error)?;
1622    prepared
1623        .history
1624        .require_unchanged_head(&prepared.reconstructed)
1625        .map_err(|error| format!("archive adoption history changed during live export: {error}"))?;
1626    let expected_internal = type_bridge_schema_compat::released_typeql_to_declared_projection(
1627        type_bridge_contract::schema::DocumentId::new("managed-fence-schema.typeql")
1628            .map_err(display)?,
1629        type_bridge_schema_migration_typedb::MANAGED_FENCE_SCHEMA_TYPEQL,
1630    )
1631    .map_err(display)?;
1632    let live = type_bridge_schema_compat::parse_adopted_genesis_authority_with_internal(
1633        type_bridge_contract::schema::DocumentId::new("legacy-live-head.typeql")
1634            .map_err(display)?,
1635        &export,
1636        Some(&expected_internal),
1637    )
1638    .map_err(display)?;
1639    if live.legacy_identity() != prepared.authority.legacy_identity()
1640        || live.declared().declared_identity_fingerprint()
1641            != prepared
1642                .authority
1643                .declared()
1644                .declared_identity_fingerprint()
1645        || live.released_extension_identity() != prepared.authority.released_extension_identity()
1646    {
1647        return Err(
1648            "live managed schema differs from the independently verified archive-head snapshot"
1649                .to_owned(),
1650        );
1651    }
1652    Ok(())
1653}
1654
1655/// Publish the bridge/genesis pair under the shared authoring lock.
1656///
1657/// The prospective complete graph is replay-verified before publication. The
1658/// bridge is made visible first, so ordinary readers fail closed during the
1659/// short incomplete interval. Exact pre-existing orphan pieces are retained
1660/// and completed; only files created by this attempt are rolled back.
1661fn publish_prepared_adoption(
1662    workspace: &TypeBridgeWorkspace,
1663    migration_directory: &type_bridge_workspace::MigrationDirectoryAuthority,
1664    prepared: &PreparedArchiveAdoption,
1665) -> Result<PathBuf, String> {
1666    publish_prepared_adoption_with_after_bridge(workspace, migration_directory, prepared, || {})
1667}
1668
1669fn publish_prepared_adoption_with_after_bridge<F>(
1670    workspace: &TypeBridgeWorkspace,
1671    migration_directory: &type_bridge_workspace::MigrationDirectoryAuthority,
1672    prepared: &PreparedArchiveAdoption,
1673    after_bridge: F,
1674) -> Result<PathBuf, String>
1675where
1676    F: FnOnce(),
1677{
1678    let directory = migration_directory.directory();
1679    let _lock = directory.try_acquire_authoring_lock().map_err(|error| {
1680        if error.kind() == std::io::ErrorKind::WouldBlock {
1681            "migration adoption conflicts with another canonical history publisher".to_owned()
1682        } else {
1683            format!("cannot lock canonical migration publication: {error}")
1684        }
1685    })?;
1686    let genesis_name = type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME;
1687    let genesis_bytes = prepared.reconstructed.schema_typeql().as_bytes();
1688    let mut bridge_created = false;
1689    let mut genesis_created = false;
1690    let mut after_bridge = Some(after_bridge);
1691
1692    let publication = (|| -> Result<(), String> {
1693        if let Some(existing) = read_existing_authority(directory, genesis_name)?
1694            && existing != genesis_bytes
1695        {
1696            return Err(format!(
1697                "{genesis_name} already exists but differs from the verified archive-head snapshot"
1698            ));
1699        }
1700        let bridge_already_published =
1701            if let Some(existing) = read_existing_authority(directory, &prepared.bridge_name)? {
1702                if existing != prepared.bridge_bytes {
1703                    return Err(format!(
1704                        "{} already exists with different authority bytes",
1705                        prepared.bridge_name
1706                    ));
1707                }
1708                true
1709            } else {
1710                false
1711            };
1712
1713        let (current, evidence) =
1714            type_bridge_schema_migration::discover_verified_migration_chain_with_evidence_in(
1715                directory,
1716                prepared.authority.declared(),
1717                workspace.delta_context(),
1718            )
1719            .map_err(display)?;
1720        let prospective = if current.manifest(prepared.bridge.id()).is_some() {
1721            current
1722        } else {
1723            let manifests = current
1724                .manifests()
1725                .map(|(_, manifest)| manifest.clone())
1726                .chain(std::iter::once(prepared.bridge.clone()))
1727                .collect::<Vec<_>>();
1728            type_bridge_schema_migration::MigrationHistoryGraph::from_verified(manifests)
1729                .map_err(display)?
1730        };
1731        type_bridge_schema_migration::require_adoption_authority_pair(&prospective, true)
1732            .map_err(display)?;
1733        evidence.require_unchanged(directory).map_err(display)?;
1734        prepared
1735            .history
1736            .require_unchanged_head(&prepared.reconstructed)
1737            .map_err(|error| {
1738                format!("archive adoption history changed before pair publication: {error}")
1739            })?;
1740
1741        if !bridge_already_published {
1742            bridge_created =
1743                publish_authority(directory, &prepared.bridge_name, &prepared.bridge_bytes)?;
1744        }
1745        if let Some(after_bridge) = after_bridge.take() {
1746            after_bridge();
1747        }
1748        prepared
1749            .history
1750            .require_unchanged_head(&prepared.reconstructed)
1751            .map_err(|error| {
1752                format!("archive adoption history changed before genesis publication: {error}")
1753            })?;
1754        genesis_created = publish_authority(directory, genesis_name, genesis_bytes)?;
1755        prepared
1756            .history
1757            .require_unchanged_head(&prepared.reconstructed)
1758            .map_err(|error| {
1759                format!("archive adoption history changed after pair publication: {error}")
1760            })?;
1761        workspace
1762            .discover_migrations_in(migration_directory)
1763            .map_err(display)?;
1764        Ok(())
1765    })();
1766
1767    if let Err(error) = publication {
1768        return Err(rollback_adoption_publication(
1769            directory,
1770            &prepared.bridge_name,
1771            bridge_created,
1772            genesis_created,
1773            error,
1774        ));
1775    }
1776    Ok(migration_directory
1777        .display_path()
1778        .join(&prepared.bridge_name))
1779}
1780
1781fn rollback_adoption_publication(
1782    directory: &type_bridge_schema_migration::MigrationDirectory,
1783    bridge_name: &str,
1784    bridge_created: bool,
1785    genesis_created: bool,
1786    primary: String,
1787) -> String {
1788    let mut cleanup_errors = Vec::new();
1789    if genesis_created
1790        && let Err(error) =
1791            directory.remove_file(type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME.as_ref())
1792    {
1793        cleanup_errors.push(format!("cannot remove newly published genesis: {error}"));
1794    }
1795    if bridge_created && let Err(error) = directory.remove_file(bridge_name.as_ref()) {
1796        cleanup_errors.push(format!("cannot remove newly published bridge: {error}"));
1797    }
1798    if (bridge_created || genesis_created)
1799        && let Err(error) = directory.sync_all()
1800    {
1801        cleanup_errors.push(format!("cannot flush adoption rollback: {error}"));
1802    }
1803    if cleanup_errors.is_empty() {
1804        primary
1805    } else {
1806        format!(
1807            "{primary}; adoption publication rollback failed: {}",
1808            cleanup_errors.join("; ")
1809        )
1810    }
1811}
1812
1813/// Publish immutable authority from a unique, flushed same-directory temp.
1814///
1815/// Hard-link publication is atomic and no-replace. An existing final name is
1816/// accepted only when its bounded bytes are identical, allowing a retry to
1817/// recover after publication succeeded but the caller did not observe it. In
1818/// particular, a directory-sync error may be reported after the final link is
1819/// already durable; that exact orphan is intentionally left for the same
1820/// adoption command to recognize and complete on retry.
1821fn publish_authority(
1822    directory: &type_bridge_schema_migration::MigrationDirectory,
1823    name: &str,
1824    bytes: &[u8],
1825) -> Result<bool, String> {
1826    use std::io::Write;
1827    if let Some(existing) = read_existing_authority(directory, name)? {
1828        if existing == bytes {
1829            return Ok(false);
1830        }
1831        return Err(format!(
1832            "{name} already exists with different authority bytes"
1833        ));
1834    }
1835    let mut temporary = None;
1836    for attempt in 0..128_u64 {
1837        let candidate = unique_authority_temporary_name(name, attempt);
1838        match directory.create_new(candidate.as_ref()) {
1839            Ok(mut file) => {
1840                if let Err(error) = file.write_all(bytes).and_then(|()| file.sync_all()) {
1841                    let _ = directory.remove_file(candidate.as_ref());
1842                    return Err(format!("cannot write {candidate}: {error}"));
1843                }
1844                temporary = Some(candidate);
1845                break;
1846            }
1847            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
1848            Err(error) => {
1849                return Err(format!("cannot create {candidate}: {error}"));
1850            }
1851        }
1852    }
1853    let temporary = temporary.ok_or_else(|| {
1854        format!("cannot allocate a unique temporary authority file beside {name}")
1855    })?;
1856    let publication = directory.hard_link(temporary.as_ref(), name.as_ref());
1857    match publication {
1858        Ok(()) => {
1859            if let Err(error) = directory.sync_all() {
1860                let _ = directory.remove_file(temporary.as_ref());
1861                return Err(format!("cannot flush migration directory: {error}"));
1862            }
1863            let _ = directory.remove_file(temporary.as_ref());
1864            directory
1865                .sync_all()
1866                .map_err(|error| format!("cannot flush migration directory: {error}"))?;
1867            Ok(true)
1868        }
1869        Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
1870            let _ = directory.remove_file(temporary.as_ref());
1871            let existing = read_existing_authority(directory, name)?
1872                .ok_or_else(|| format!("{name} disappeared during no-replace publication"))?;
1873            if existing == bytes {
1874                Ok(false)
1875            } else {
1876                Err(format!(
1877                    "{name} was concurrently published with different authority bytes"
1878                ))
1879            }
1880        }
1881        Err(error) => {
1882            let _ = directory.remove_file(temporary.as_ref());
1883            Err(format!("cannot publish {name}: {error}"))
1884        }
1885    }
1886}
1887
1888fn read_existing_authority(
1889    directory: &type_bridge_schema_migration::MigrationDirectory,
1890    name: &str,
1891) -> Result<Option<Vec<u8>>, String> {
1892    use std::io::Read;
1893    let file = match directory.open_regular_readonly(name.as_ref()) {
1894        Ok(file) => file,
1895        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1896        Err(error) => return Err(format!("cannot read {name}: {error}")),
1897    };
1898    let limit = type_bridge_contract::limits::MAX_CANONICAL_BYTES;
1899    let mut bytes = Vec::new();
1900    file.take(u64::try_from(limit).unwrap_or(u64::MAX).saturating_add(1))
1901        .read_to_end(&mut bytes)
1902        .map_err(|error| format!("cannot read {name}: {error}"))?;
1903    if bytes.len() > limit {
1904        return Err(format!("{name} exceeds the 16 MiB authority ceiling"));
1905    }
1906    Ok(Some(bytes))
1907}
1908
1909fn unique_authority_temporary_name(name: &str, attempt: u64) -> String {
1910    use std::sync::atomic::{AtomicU64, Ordering};
1911    static NEXT_AUTHORITY_TEMPORARY: AtomicU64 = AtomicU64::new(1);
1912    let nonce = NEXT_AUTHORITY_TEMPORARY.fetch_add(1, Ordering::Relaxed);
1913    format!(".{name}.{}.{}.{}.tmp", std::process::id(), nonce, attempt)
1914}
1915
1916fn resolve_credential(
1917    reference: &type_bridge_workspace::SecretReference,
1918) -> Result<String, String> {
1919    std::env::var(reference.environment_variable()).map_err(|_| {
1920        format!(
1921            "credential environment variable {:?} is not set",
1922            reference.environment_variable()
1923        )
1924    })
1925}
1926
1927#[cfg(test)]
1928mod credential_error_redaction_tests {
1929    use super::*;
1930    use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
1931    use type_bridge_typedb_runtime::RuntimeError;
1932
1933    const PROVIDER_TEXT: &str =
1934        "TB_ADDRESS_SECRET TB_USERNAME_SECRET TB_PASSWORD_SECRET TB_PROVIDER_SECRET";
1935    const SECRETS: [&str; 4] = [
1936        "TB_ADDRESS_SECRET",
1937        "TB_USERNAME_SECRET",
1938        "TB_PASSWORD_SECRET",
1939        "TB_PROVIDER_SECRET",
1940    ];
1941
1942    fn hostile_secure_error() -> type_bridge_orm::SecureConnectError {
1943        type_bridge_orm::SecureConnectError::Runtime(RuntimeError::Connection(
1944            PROVIDER_TEXT.to_owned(),
1945        ))
1946    }
1947
1948    #[test]
1949    fn connected_lifecycle_contexts_drop_hostile_provider_text() {
1950        for (context, code) in [
1951            (
1952                "cannot check database \"managed\"",
1953                "typedb_database_exists_failed",
1954            ),
1955            (
1956                "cannot ensure database \"managed\"",
1957                "typedb_database_ensure_failed",
1958            ),
1959            (
1960                "cannot connect the managed database",
1961                "typedb_database_connect_failed",
1962            ),
1963        ] {
1964            let sanitized =
1965                sanitize_connected_error(context.to_owned(), code, hostile_secure_error());
1966            let rendered = format!("{sanitized}\n{sanitized:?}");
1967            for secret in SECRETS {
1968                assert!(!rendered.contains(secret), "{secret}: {rendered}");
1969            }
1970            assert!(rendered.contains(context), "{rendered}");
1971            assert!(rendered.contains(code), "{rendered}");
1972        }
1973    }
1974
1975    #[test]
1976    fn connected_orm_lifecycle_contexts_drop_hostile_provider_text() {
1977        for (context, code) in [
1978            (
1979                "cannot check database \"managed\"",
1980                "typedb_database_exists_failed",
1981            ),
1982            (
1983                "cannot ensure database \"managed\"",
1984                "typedb_database_ensure_failed",
1985            ),
1986        ] {
1987            let sanitized = sanitize_connected_orm_error(
1988                context.to_owned(),
1989                code,
1990                type_bridge_orm::OrmError::Connection(PROVIDER_TEXT.to_owned()),
1991            );
1992            let rendered = format!("{sanitized}\n{sanitized:?}");
1993            for secret in SECRETS {
1994                assert!(!rendered.contains(secret), "{secret}: {rendered}");
1995            }
1996            assert!(rendered.contains(context), "{rendered}");
1997            assert!(rendered.contains(code), "{rendered}");
1998        }
1999    }
2000
2001    #[test]
2002    fn connected_lifecycle_preserves_only_typed_safe_diagnostics() {
2003        let sanitized = sanitize_connected_error(
2004            "cannot connect the managed database".to_owned(),
2005            "typedb_database_connect_failed",
2006            type_bridge_orm::SecureConnectError::DriverTlsConfiguration { band: 9 },
2007        );
2008        assert!(
2009            sanitized.contains("tls_driver_lowering_failed"),
2010            "{sanitized}"
2011        );
2012        assert!(sanitized.contains("driver band 9"), "{sanitized}");
2013        assert!(
2014            !sanitized.contains("typedb_database_connect_failed"),
2015            "{sanitized}"
2016        );
2017    }
2018
2019    #[test]
2020    fn schema_export_drops_hostile_orm_text_and_source() {
2021        let sanitized = sanitize_schema_export_error(type_bridge_orm::OrmError::Connection(
2022            PROVIDER_TEXT.into(),
2023        ));
2024        let rendered = format!("{sanitized}\n{sanitized:?}");
2025        for secret in SECRETS {
2026            assert!(!rendered.contains(secret), "{secret}: {rendered}");
2027        }
2028        assert!(
2029            rendered.contains("typedb_schema_export_failed"),
2030            "{rendered}"
2031        );
2032    }
2033
2034    #[test]
2035    fn migration_runner_display_omits_provider_details() {
2036        let diagnostic = Diagnostic::new(
2037            DiagnosticCategory::InvalidContract,
2038            DiagnosticCode::new("migration_provider_test_failed").expect("static code"),
2039            "migration provider operation failed",
2040        )
2041        .with_detail("provider", PROVIDER_TEXT);
2042        let error = type_bridge_schema_migration_typedb::MigrationDirectoryApplyError::Diagnostic(
2043            diagnostic,
2044        );
2045
2046        let rendered = display(error);
2047        for secret in SECRETS {
2048            assert!(!rendered.contains(secret), "{secret}: {rendered}");
2049        }
2050        assert!(
2051            rendered.contains("migration_provider_test_failed"),
2052            "{rendered}"
2053        );
2054    }
2055
2056    #[test]
2057    fn migration_outcome_projection_omits_provider_details_for_apply_and_adopt() {
2058        use type_bridge_contract::migration::MigrationId;
2059        use type_bridge_schema_migration::{MigrationExecutionOutcome, MigrationExecutionPosition};
2060
2061        let diagnostic = || {
2062            Diagnostic::new(
2063                DiagnosticCategory::InvalidContract,
2064                DiagnosticCode::new("migration_provider_test_failed").expect("static code"),
2065                "migration provider operation failed",
2066            )
2067            .with_detail("provider", PROVIDER_TEXT)
2068        };
2069        for context in [
2070            "apply did not complete",
2071            "adoption checkpoint did not complete",
2072        ] {
2073            for (outcome, expected_state, expected_position) in [
2074                (
2075                    MigrationExecutionOutcome::RetrySafe {
2076                        migration_id: MigrationId::new("example", "0001_initial")
2077                            .expect("migration id"),
2078                        position: MigrationExecutionPosition::TransactionGroup(7),
2079                        diagnostic: diagnostic(),
2080                    },
2081                    "retry-safe",
2082                    "transaction group 7",
2083                ),
2084                (
2085                    MigrationExecutionOutcome::RequiresExplicitRecovery {
2086                        migration_id: MigrationId::new("example", "0001_initial")
2087                            .expect("migration id"),
2088                        position: MigrationExecutionPosition::ManifestCheckpoint,
2089                        diagnostic: diagnostic(),
2090                    },
2091                    "explicit recovery required",
2092                    "manifest checkpoint",
2093                ),
2094            ] {
2095                let rendered = sanitize_migration_execution_outcome(context, outcome);
2096                for secret in SECRETS {
2097                    assert!(!rendered.contains(secret), "{secret}: {rendered}");
2098                }
2099                for expected in [
2100                    context,
2101                    expected_state,
2102                    "example/0001_initial",
2103                    expected_position,
2104                    "migration_provider_test_failed",
2105                    "migration provider operation failed",
2106                ] {
2107                    assert!(rendered.contains(expected), "{expected}: {rendered}");
2108                }
2109            }
2110        }
2111    }
2112}
2113
2114#[cfg(all(test, unix))]
2115mod output_authority_tests {
2116    use super::*;
2117    use std::os::unix::fs::symlink;
2118
2119    #[test]
2120    fn retained_output_authority_survives_component_swap_without_redirecting() {
2121        let workspace = tempfile::tempdir().expect("workspace directory");
2122        let outside = tempfile::tempdir().expect("outside directory");
2123        fs::create_dir_all(workspace.path().join("generated/python")).expect("output directory");
2124        let authority = WorkspaceDirectoryAuthority::open(
2125            WorkspaceRoot::new(fs::canonicalize(workspace.path()).expect("canonical workspace"))
2126                .expect("workspace root"),
2127        )
2128        .expect("workspace authority");
2129        let root = authority.output_root().expect("output authority");
2130        let output = root
2131            .open_beneath(Path::new("generated/python"))
2132            .expect("output authority");
2133
2134        let held = workspace.path().join("generated/python-held");
2135        fs::rename(workspace.path().join("generated/python"), &held)
2136            .expect("move retained output directory");
2137        symlink(outside.path(), workspace.path().join("generated/python"))
2138            .expect("redirect configured output path");
2139
2140        output
2141            .write_atomic("_models.py".as_ref(), b"retained authority")
2142            .expect("publication remains handle-relative");
2143        assert_eq!(
2144            fs::read(held.join("_models.py")).expect("retained output reads"),
2145            b"retained authority"
2146        );
2147        assert!(
2148            !outside.path().join("_models.py").exists(),
2149            "component replacement redirected output outside the workspace"
2150        );
2151    }
2152
2153    #[test]
2154    fn retained_output_root_survives_root_entry_swap_without_redirecting() {
2155        let workspace = tempfile::tempdir().expect("workspace directory");
2156        let outside = tempfile::tempdir().expect("outside directory");
2157        fs::create_dir_all(workspace.path().join("generated/python")).expect("output directory");
2158        let authority = WorkspaceDirectoryAuthority::open(
2159            WorkspaceRoot::new(fs::canonicalize(workspace.path()).expect("canonical workspace"))
2160                .expect("workspace root"),
2161        )
2162        .expect("workspace authority");
2163        let root = authority.output_root().expect("output authority");
2164        let held = workspace
2165            .path()
2166            .parent()
2167            .expect("temporary parent")
2168            .join(format!(
2169                "{}-retained",
2170                workspace
2171                    .path()
2172                    .file_name()
2173                    .expect("temporary name")
2174                    .to_string_lossy()
2175            ));
2176        fs::rename(workspace.path(), &held).expect("workspace root moves after validation");
2177        symlink(outside.path(), workspace.path()).expect("workspace name redirects outside");
2178
2179        let output = root
2180            .open_beneath(Path::new("generated/python"))
2181            .expect("output opens through retained root");
2182        output
2183            .write_atomic("_models.py".as_ref(), b"retained root authority")
2184            .expect("publication remains rooted in the retained handle");
2185        assert_eq!(
2186            fs::read(held.join("generated/python/_models.py")).expect("retained output reads"),
2187            b"retained root authority"
2188        );
2189        assert!(
2190            !outside.path().join("generated/python/_models.py").exists(),
2191            "root replacement redirected output outside the workspace"
2192        );
2193
2194        fs::remove_file(workspace.path()).expect("replacement symlink removes");
2195        fs::rename(&held, workspace.path()).expect("workspace restores for cleanup");
2196    }
2197}
2198
2199fn bind_approvals(
2200    graph: &type_bridge_schema_migration::MigrationHistoryGraph,
2201    approvals: &[String],
2202) -> Result<Vec<type_bridge_schema_migration::MigrationApplyApproval>, String> {
2203    if approvals.is_empty() {
2204        return Ok(Vec::new());
2205    }
2206    approvals
2207        .iter()
2208        .map(|compound| {
2209            let (app_label, name) = compound
2210                .split_once('/')
2211                .ok_or_else(|| format!("approval {compound:?} must be app-label/name"))?;
2212            let id = type_bridge_contract::migration::MigrationId::from_components(
2213                type_bridge_contract::migration::MigrationAppLabel::new(app_label.to_owned())
2214                    .map_err(display)?,
2215                type_bridge_contract::migration::MigrationName::new(name.to_owned())
2216                    .map_err(display)?,
2217            );
2218            let manifest = graph.manifest(&id).ok_or_else(|| {
2219                format!("approval target {compound:?} is not in the committed history")
2220            })?;
2221            type_bridge_schema_migration::MigrationApplyApproval::for_manifest(manifest)
2222                .map_err(display)
2223        })
2224        .collect()
2225}
2226
2227fn parse_migration_ids(
2228    graph: &type_bridge_schema_migration::MigrationHistoryGraph,
2229    values: &[String],
2230    kind: &str,
2231) -> Result<BTreeSet<type_bridge_contract::migration::MigrationId>, String> {
2232    let mut ids = BTreeSet::new();
2233    for compound in values {
2234        let (app_label, name) = compound
2235            .split_once('/')
2236            .ok_or_else(|| format!("{kind} {compound:?} must be app-label/name"))?;
2237        let id = type_bridge_contract::migration::MigrationId::from_components(
2238            type_bridge_contract::migration::MigrationAppLabel::new(app_label.to_owned())
2239                .map_err(display)?,
2240            type_bridge_contract::migration::MigrationName::new(name.to_owned())
2241                .map_err(display)?,
2242        );
2243        if graph.manifest(&id).is_none() {
2244            return Err(format!(
2245                "{kind} target {compound:?} is not in the committed history"
2246            ));
2247        }
2248        if !ids.insert(id) {
2249            return Err(format!("{kind} target {compound:?} is duplicated"));
2250        }
2251    }
2252    Ok(ids)
2253}
2254
2255fn bind_rollback_approvals(
2256    graph: &type_bridge_schema_migration::MigrationHistoryGraph,
2257    approvals: &[String],
2258) -> Result<Vec<type_bridge_schema_migration::MigrationApplyApproval>, String> {
2259    let ids = parse_migration_ids(graph, approvals, "rollback approval")?;
2260    let mut bound = Vec::new();
2261    for id in ids {
2262        let manifest = graph
2263            .manifest(&id)
2264            .ok_or_else(|| "internal rollback approval target disappeared".to_owned())?;
2265        for safety in [
2266            type_bridge_schema::SafetyClass::FormalOnly,
2267            type_bridge_schema::SafetyClass::SchemaMetadata,
2268            type_bridge_schema::SafetyClass::Additive,
2269            type_bridge_schema::SafetyClass::Conditional,
2270            type_bridge_schema::SafetyClass::Destructive,
2271            type_bridge_schema::SafetyClass::Opaque,
2272        ] {
2273            bound.push(
2274                type_bridge_schema_migration::MigrationApplyApproval::for_rollback(
2275                    manifest, safety,
2276                )
2277                .map_err(display)?,
2278            );
2279        }
2280    }
2281    Ok(bound)
2282}
2283
2284#[cfg(test)]
2285mod transport_option_tests {
2286    use super::*;
2287
2288    fn environment(policy: WorkspaceTransportPolicy) -> WorkspaceEnvironment {
2289        WorkspaceEnvironment::new(
2290            "typedb.example:1729",
2291            "example",
2292            SecretReference::environment("TYPEBRIDGE_TEST_USERNAME").expect("username reference"),
2293            SecretReference::environment("TYPEBRIDGE_TEST_PASSWORD").expect("password reference"),
2294        )
2295        .expect("environment")
2296        .with_transport_policy(policy)
2297    }
2298
2299    fn custom_root_workspace(root_bytes: &[u8]) -> (tempfile::TempDir, TypeBridgeWorkspace) {
2300        let directory = tempfile::tempdir().expect("workspace directory");
2301        fs::create_dir_all(directory.path().join("schema/fragments")).expect("schema directory");
2302        fs::create_dir_all(directory.path().join("migrations/v2")).expect("migration directory");
2303        fs::create_dir_all(directory.path().join("certs")).expect("certificate directory");
2304        fs::write(
2305            directory.path().join("schema/schema.yaml"),
2306            "format: typebridge.schema-set/v1\nsources: [fragments/*.yaml]\n",
2307        )
2308        .expect("schema set writes");
2309        fs::write(
2310            directory.path().join("schema/fragments/model.yaml"),
2311            "format: typebridge.schema/v2\nentities: {person: {}}\n",
2312        )
2313        .expect("schema writes");
2314        fs::write(directory.path().join("certs/root.pem"), root_bytes).expect("certificate writes");
2315        let manifest = directory.path().join("typebridge.yaml");
2316        fs::write(
2317            &manifest,
2318            "format: typebridge.workspace/v1\n\
2319             schema:\n  root: schema/schema.yaml\n  ownership: exclusive\n  managed-scope: tls-test\n\
2320             compatibility:\n  semantic-profile: typedb-3.12.1/v1\n\
2321             migrations:\n  directory: migrations/v2\n  app-label: tlstest\n\
2322             environments:\n  dev:\n    database: tls_test\n    uri: never-contact.invalid:1729\n    \
2323             tls: 'true'\n    tls-root-ca: certs/root.pem\n    credential:\n      username: \
2324             env:TYPEBRIDGE_TEST_USERNAME\n      password: env:TYPEBRIDGE_TEST_PASSWORD\n",
2325        )
2326        .expect("manifest writes");
2327        let workspace = load_workspace(&manifest).expect("custom-root workspace loads");
2328        (directory, workspace)
2329    }
2330
2331    #[test]
2332    fn workspace_transport_policy_maps_without_changing_plaintext_defaults() {
2333        let defaults = type_bridge_orm::SecureConnectOptions::default();
2334        let disabled = secure_connect_options(&environment(WorkspaceTransportPolicy::Disabled));
2335        assert_eq!(disabled.tls_mode, type_bridge_orm::TlsMode::Disabled);
2336        assert_eq!(disabled.http_port, defaults.http_port);
2337        assert_eq!(disabled.server_version, defaults.server_version);
2338
2339        let native = secure_connect_options(
2340            &environment(WorkspaceTransportPolicy::NativeRoots).with_http_port(9443),
2341        );
2342        assert_eq!(native.tls_mode, type_bridge_orm::TlsMode::NativeRoots);
2343        assert_eq!(native.http_port, 9443);
2344        assert_eq!(native.server_version, defaults.server_version);
2345    }
2346
2347    #[test]
2348    fn custom_root_mapping_preserves_the_validated_canonical_path() {
2349        let directory = tempfile::tempdir().expect("workspace directory");
2350        let canonical = fs::canonicalize(directory.path()).expect("canonical workspace");
2351        fs::create_dir_all(canonical.join("certs")).expect("certificate directory");
2352        fs::write(
2353            canonical.join("certs/root.pem"),
2354            b"not parsed at workspace boundary\n",
2355        )
2356        .expect("certificate writes");
2357        let root = WorkspaceRoot::new(canonical.clone()).expect("workspace root");
2358        let root_ca = type_bridge_workspace::WorkspaceRootCa::new(
2359            &root,
2360            "certs/root.pem",
2361            &SystemSchemaSourceService,
2362        )
2363        .expect("confined root CA");
2364
2365        let options = secure_connect_options(
2366            &environment(WorkspaceTransportPolicy::CustomRootCa(root_ca)).with_http_port(8443),
2367        );
2368        assert_eq!(
2369            options.tls_mode,
2370            type_bridge_orm::TlsMode::CustomRootCa(canonical.join("certs/root.pem"))
2371        );
2372        assert_eq!(options.http_port, 8443);
2373    }
2374
2375    #[test]
2376    fn malformed_custom_root_fails_transport_preflight_before_credentials_are_needed() {
2377        let (_directory, workspace) = custom_root_workspace(b"definitely not a certificate\n");
2378
2379        let error = preflight_secure_connect_options(&workspace, "dev")
2380            .expect_err("PEM parsing must happen before credential resolution");
2381        assert!(error.contains("tls_custom_root_ca_invalid_pem"), "{error}");
2382        assert!(!error.contains("TYPEBRIDGE_TEST_USERNAME"), "{error}");
2383        assert!(!error.contains("TYPEBRIDGE_TEST_PASSWORD"), "{error}");
2384    }
2385
2386    #[cfg(unix)]
2387    #[test]
2388    fn workspace_root_swap_to_outside_symlink_is_rejected_at_transport_preflight() {
2389        use std::os::unix::fs::symlink;
2390
2391        let (directory, workspace) = custom_root_workspace(b"initial regular root\n");
2392        let outside = tempfile::tempdir().expect("outside directory");
2393        let configured = directory.path().join("certs/root.pem");
2394
2395        let outside_root = outside.path().join("malicious.pem");
2396        fs::write(
2397            &outside_root,
2398            include_bytes!("../../core/tests/fixtures/valid-root.pem"),
2399        )
2400        .expect("write outside replacement root");
2401        fs::remove_file(&configured).expect("remove validated confined root");
2402        symlink(&outside_root, &configured).expect("install outside symlink after validation");
2403
2404        let error = preflight_secure_connect_options(&workspace, "dev")
2405            .expect_err("retained workspace paths must never follow a replacement symlink");
2406        assert!(error.contains("tls_custom_root_ca_unreadable"), "{error}");
2407        assert!(!error.contains("tls_custom_root_ca_invalid_pem"), "{error}");
2408    }
2409
2410    #[cfg(unix)]
2411    #[test]
2412    fn real_directory_root_replacement_cannot_substitute_custom_trust() {
2413        let (directory, workspace) =
2414            custom_root_workspace(include_bytes!("../../core/tests/fixtures/valid-root.pem"));
2415        let configured_root = directory.path().to_path_buf();
2416        let held_root = configured_root.with_extension("retained-custom-root-ca");
2417        fs::rename(&configured_root, &held_root).expect("move retained workspace root");
2418        fs::create_dir_all(configured_root.join("certs")).expect("replacement root creates");
2419        fs::write(
2420            configured_root.join("certs/root.pem"),
2421            b"attacker-controlled replacement is not a certificate\n",
2422        )
2423        .expect("replacement root writes");
2424
2425        let preflight = preflight_secure_connect_options(&workspace, "dev");
2426
2427        fs::remove_dir_all(&configured_root).expect("replacement root removes");
2428        fs::rename(&held_root, &configured_root).expect("retained root restores");
2429        preflight.expect("transport must use the CA under the retained original root");
2430    }
2431}
2432
2433#[cfg(test)]
2434mod rollback_cli_contract_tests {
2435    use super::*;
2436
2437    #[test]
2438    fn rollback_grammar_requires_environment_and_explicit_removal() {
2439        assert!(Cli::try_parse_from(["type-bridge", "migration", "rollback"]).is_err());
2440        assert!(
2441            Cli::try_parse_from([
2442                "type-bridge",
2443                "migration",
2444                "rollback",
2445                "--environment",
2446                "live",
2447            ])
2448            .is_err()
2449        );
2450        let cli = Cli::try_parse_from([
2451            "type-bridge",
2452            "migration",
2453            "rollback",
2454            "--environment",
2455            "live",
2456            "--remove",
2457            "example/0002_contract",
2458            "--remove",
2459            "example/0001_expand",
2460            "--approve",
2461            "example/0002_contract",
2462            "--output",
2463            "json",
2464        ])
2465        .expect("explicit preview grammar parses");
2466        let Command::Migration {
2467            command:
2468                MigrationCommand::Rollback {
2469                    environment,
2470                    removals,
2471                    approvals,
2472                    execute,
2473                    output,
2474                },
2475        } = cli.command
2476        else {
2477            panic!("rollback command parsed into another command")
2478        };
2479        assert_eq!(environment, "live");
2480        assert_eq!(removals.len(), 2);
2481        assert_eq!(approvals, ["example/0002_contract"]);
2482        assert!(!execute, "preview is the non-mutating default");
2483        assert_eq!(output, RollbackOutput::Json);
2484    }
2485
2486    #[test]
2487    fn rollback_execution_requires_an_explicit_flag() {
2488        let cli = Cli::try_parse_from([
2489            "type-bridge",
2490            "migration",
2491            "rollback",
2492            "--environment",
2493            "live",
2494            "--remove",
2495            "example/0002_contract",
2496            "--execute",
2497        ])
2498        .expect("explicit execution grammar parses");
2499        assert!(matches!(
2500            cli.command,
2501            Command::Migration {
2502                command: MigrationCommand::Rollback { execute: true, .. }
2503            }
2504        ));
2505    }
2506}
2507
2508#[cfg(test)]
2509mod migration_command_tests {
2510    use super::*;
2511
2512    fn write_workspace_manifest(root: &Path, semantic_profile: &str) -> PathBuf {
2513        fs::create_dir_all(root.join("schema/fragments")).expect("schema directory");
2514        fs::write(
2515            root.join("schema/schema.yaml"),
2516            "format: typebridge.schema-set/v1\nsources: [fragments/*.yaml]\n",
2517        )
2518        .expect("schema set writes");
2519        fs::write(
2520            root.join("schema/fragments/model.yaml"),
2521            "format: typebridge.schema/v2\nentities: {person: {}}\n",
2522        )
2523        .expect("schema writes");
2524        let manifest = root.join("typebridge.yaml");
2525        fs::write(
2526            &manifest,
2527            format!(
2528                "format: typebridge.workspace/v1\n\
2529                 schema:\n  root: schema/schema.yaml\n  ownership: exclusive\n  managed-scope: command-test\n\
2530                 compatibility:\n  semantic-profile: {semantic_profile}\n\
2531                 migrations:\n  directory: migrations/v2\n  app-label: commandtest\n\
2532                 environments:\n  dev:\n    database: command_test\n    uri: never-contact.invalid:1729\n    migrate: 'true'\n    credential:\n      username: env:TYPEBRIDGE_COMMAND_TEST_USERNAME\n      password: env:TYPEBRIDGE_COMMAND_TEST_PASSWORD\n"
2533            ),
2534        )
2535        .expect("manifest writes");
2536        manifest
2537    }
2538
2539    #[test]
2540    fn migration_make_creates_its_missing_authoring_directory() {
2541        let directory = tempfile::tempdir().expect("workspace directory");
2542        let manifest = write_workspace_manifest(directory.path(), "typedb-3.11.5/v1");
2543        let migration_directory = directory.path().join("migrations/v2");
2544        assert!(!migration_directory.exists());
2545
2546        run(&Cli {
2547            manifest,
2548            command: Command::Migration {
2549                command: MigrationCommand::Make {
2550                    name: "initial".to_owned(),
2551                    backfill_intent: None,
2552                },
2553            },
2554        })
2555        .expect("migration make creates and publishes into its authoring directory");
2556
2557        assert!(
2558            migration_directory
2559                .join("0001_initial.tbmigration.json")
2560                .is_file()
2561        );
2562        assert!(migration_directory.join("0001_initial.typeql").is_file());
2563    }
2564
2565    #[test]
2566    fn migration_make_accepts_a_confined_backfill_intent() {
2567        let directory = tempfile::tempdir().expect("workspace directory");
2568        let manifest = write_workspace_manifest(directory.path(), "typedb-3.11.5/v1");
2569        fs::write(
2570            directory.path().join("schema/fragments/model.yaml"),
2571            "format: typebridge.schema/v2\nattributes:\n  display-name: { value: string }\n  legacy-name: { value: string }\n  person-id: { value: string }\nentities:\n  person:\n    owns:\n      display-name: {}\n      legacy-name: {}\n      person-id: { key: true }\n",
2572        )
2573        .expect("backfill schema writes");
2574        let initial = Cli {
2575            manifest: manifest.clone(),
2576            command: Command::Migration {
2577                command: MigrationCommand::Make {
2578                    name: "initial".to_owned(),
2579                    backfill_intent: None,
2580                },
2581            },
2582        };
2583        run(&initial).expect("initial migration");
2584        fs::write(
2585            directory.path().join("migrations/v2/copy-name.backfill.yaml"),
2586            "format: typebridge.migration-backfill-intent/v1\ncopy-attribute:\n  owner-kind: entity\n  owner: person\n  source: legacy-name\n  destination: display-name\n  partition-key: person-id\n  batch-rows: 128\n  reverse: remove-equal-copied-destination\n",
2587        )
2588        .expect("backfill intent writes");
2589
2590        run(&Cli {
2591            manifest,
2592            command: Command::Migration {
2593                command: MigrationCommand::Make {
2594                    name: "copy-name".to_owned(),
2595                    backfill_intent: Some(PathBuf::from("copy-name.backfill.yaml")),
2596                },
2597            },
2598        })
2599        .expect("backfill migration authors");
2600
2601        assert!(
2602            directory
2603                .path()
2604                .join("migrations/v2/0002_copy-name.tbmigration.json")
2605                .is_file()
2606        );
2607    }
2608
2609    #[test]
2610    fn unsupported_execution_profile_rejects_before_credentials_or_filesystem_mutation() {
2611        let directory = tempfile::tempdir().expect("workspace directory");
2612        let manifest = write_workspace_manifest(directory.path(), "typedb-3.11.5/v1");
2613        let workspace = load_workspace(&manifest).expect("workspace loads");
2614
2615        for action in [
2616            ConnectedAction::Apply {
2617                approvals: Vec::new(),
2618            },
2619            ConnectedAction::Verify,
2620            ConnectedAction::Adopt {
2621                archive_directory: directory.path().join("missing-archive"),
2622                name: "0000_archive_frontier".to_owned(),
2623            },
2624        ] {
2625            let error = run_connected(&workspace, "dev", action)
2626                .expect_err("every connected migration operation uses the exact profile");
2627
2628            assert!(
2629                error.contains("migration_typedb_semantic_profile_unsupported"),
2630                "{error}"
2631            );
2632            assert!(error.contains("typedb-3.11.5/v1"), "{error}");
2633            assert!(error.contains("typedb-3.12.1/v1"), "{error}");
2634            assert!(
2635                !error.contains("credential environment variable")
2636                    && !error.contains("cannot connect")
2637                    && !error.contains("cannot check database"),
2638                "profile gate ran after external setup: {error}"
2639            );
2640        }
2641        assert!(
2642            !directory.path().join("migrations/v2").exists(),
2643            "profile rejection must not create the migration directory"
2644        );
2645    }
2646}
2647
2648#[cfg(test)]
2649mod adoption_file_tests {
2650    use super::*;
2651    use sha2::{Digest as _, Sha256};
2652
2653    const LEGACY_SCHEMA: &str = "define\nentity person;\n";
2654
2655    fn adoption_workspace() -> (tempfile::TempDir, TypeBridgeWorkspace) {
2656        let directory = tempfile::tempdir().expect("workspace directory");
2657        fs::create_dir_all(directory.path().join("schema/fragments")).expect("schema directory");
2658        fs::write(
2659            directory.path().join("schema/schema.yaml"),
2660            "format: typebridge.schema-set/v1\nsources: [fragments/*.yaml]\n",
2661        )
2662        .expect("schema set writes");
2663        fs::write(
2664            directory.path().join("schema/fragments/model.yaml"),
2665            "format: typebridge.schema/v2\nentities: {person: {}}\n",
2666        )
2667        .expect("schema writes");
2668        let manifest = directory.path().join("typebridge.yaml");
2669        fs::write(
2670            &manifest,
2671            "format: typebridge.workspace/v1\n\
2672             schema:\n  root: schema/schema.yaml\n  ownership: exclusive\n  managed-scope: adoption-test\n\
2673             compatibility:\n  semantic-profile: typedb-3.12.1/v1\n\
2674             migrations:\n  directory: migrations/v2\n  app-label: smoke\n\
2675             environments:\n  dev:\n    database: adoption_test\n    uri: never-contact.invalid:1729\n    migrate: 'true'\n    credential:\n      username: env:TYPEBRIDGE_TEST_USERNAME\n      password: env:TYPEBRIDGE_TEST_PASSWORD\n",
2676        )
2677        .expect("manifest writes");
2678        let workspace = load_workspace(&manifest).expect("workspace loads");
2679        (directory, workspace)
2680    }
2681
2682    fn write_legacy_fixture(root: &Path) -> PathBuf {
2683        let directory = root.join("migrations/legacy");
2684        fs::create_dir_all(&directory).expect("legacy directory");
2685        let name = "0001_initial";
2686        let python_source = "class Migration:\n    operations = []\n";
2687        let checksum = type_bridge_migration::migration_file_checksum(python_source);
2688        let source_sha256 = format!("{:x}", Sha256::digest(python_source.as_bytes()));
2689        let schema_hash = format!("{:x}", Sha256::digest(LEGACY_SCHEMA.as_bytes()));
2690        fs::write(directory.join(format!("{name}.py")), python_source)
2691            .expect("legacy source writes");
2692        let adoption = type_bridge_migration::LegacyAdoptionMetadata::new(
2693            "legacy",
2694            name,
2695            Vec::new(),
2696            checksum,
2697            source_sha256,
2698            type_bridge_migration::LegacySchemaEffect::Snapshot,
2699            type_bridge_migration::MigrationDependencySpec {
2700                app_label: "legacy".to_owned(),
2701                migration_name: name.to_owned(),
2702            },
2703            schema_hash.clone(),
2704        )
2705        .expect("legacy adoption metadata");
2706        fs::write(
2707            directory.join(format!("{name}.adoption.json")),
2708            serde_json::to_vec_pretty(&adoption).expect("metadata encodes"),
2709        )
2710        .expect("metadata writes");
2711        let snapshot = directory.join("snapshots/v0001");
2712        fs::create_dir_all(&snapshot).expect("snapshot directory");
2713        fs::write(snapshot.join("schema.tql"), LEGACY_SCHEMA).expect("snapshot schema writes");
2714        fs::write(
2715            snapshot.join("snapshot.json"),
2716            serde_json::to_vec_pretty(&serde_json::json!({
2717                "version": "v0001",
2718                "source_migration": name,
2719                "schema_hash": schema_hash,
2720                "file_hashes": {"schema.tql": schema_hash},
2721                "type_bridge_version": "1.5.11",
2722                "type_bridge_core_version": "1.5.11"
2723            }))
2724            .expect("snapshot manifest encodes"),
2725        )
2726        .expect("snapshot manifest writes");
2727        directory
2728    }
2729
2730    #[test]
2731    fn authority_publication_is_atomic_no_replace_and_resumable() {
2732        let directory = tempfile::tempdir().expect("directory");
2733        let authority =
2734            type_bridge_schema_migration::MigrationDirectory::open_ambient(directory.path())
2735                .expect("directory authority");
2736        let name = "adopted-genesis.typeql";
2737        let path = directory.path().join("adopted-genesis.typeql");
2738        assert!(
2739            publish_authority(&authority, name, b"define\nentity person;\n")
2740                .expect("first publish")
2741        );
2742        assert!(
2743            !publish_authority(&authority, name, b"define\nentity person;\n")
2744                .expect("identical recovery")
2745        );
2746        assert!(publish_authority(&authority, name, b"define\nentity company;\n").is_err());
2747        assert_eq!(
2748            fs::read(&path).expect("authority reads"),
2749            b"define\nentity person;\n"
2750        );
2751        assert!(
2752            fs::read_dir(directory.path())
2753                .expect("directory reads")
2754                .all(|entry| !entry
2755                    .expect("entry")
2756                    .file_name()
2757                    .to_string_lossy()
2758                    .ends_with(".tmp"))
2759        );
2760    }
2761
2762    #[cfg(unix)]
2763    #[test]
2764    fn authority_publication_rejects_final_symlink() {
2765        use std::os::unix::fs::symlink;
2766
2767        let directory = tempfile::tempdir().expect("directory");
2768        let outside = directory.path().join("outside");
2769        fs::write(&outside, b"untouched").expect("outside writes");
2770        let path = directory.path().join("adopted-genesis.typeql");
2771        symlink(&outside, &path).expect("symlink");
2772        let authority =
2773            type_bridge_schema_migration::MigrationDirectory::open_ambient(directory.path())
2774                .expect("directory authority");
2775
2776        assert!(publish_authority(&authority, "adopted-genesis.typeql", b"replacement").is_err());
2777        assert_eq!(fs::read(&outside).expect("outside reads"), b"untouched");
2778    }
2779
2780    #[test]
2781    fn invalid_adoption_name_creates_no_canonical_directory() {
2782        let (directory, workspace) = adoption_workspace();
2783        let missing_archive = directory.path().join("missing-legacy");
2784        let error = run_connected(
2785            &workspace,
2786            "dev",
2787            ConnectedAction::Adopt {
2788                archive_directory: missing_archive,
2789                name: String::new(),
2790            },
2791        )
2792        .expect_err("invalid name fails before history or network access");
2793        assert!(error.contains("migration"), "{error}");
2794        assert!(
2795            !directory.path().join("migrations/v2").exists(),
2796            "bad-name validation must not create canonical filesystem state"
2797        );
2798    }
2799
2800    #[test]
2801    fn invalid_apply_approvals_fail_before_credentials_or_network() {
2802        let (directory, workspace) = adoption_workspace();
2803        fs::create_dir_all(directory.path().join("migrations/v2")).expect("canonical directory");
2804
2805        for (approval, expected) in [
2806            ("not-a-compound-id", "must be app-label/name"),
2807            ("smoke/0001_missing", "is not in the committed history"),
2808        ] {
2809            let error = run_connected(
2810                &workspace,
2811                "dev",
2812                ConnectedAction::Apply {
2813                    approvals: vec![approval.to_owned()],
2814                },
2815            )
2816            .expect_err("invalid approval is rejected by local authority");
2817            assert!(error.contains(expected), "{approval}: {error}");
2818            assert!(
2819                !error.contains("credential")
2820                    && !error.contains("connect")
2821                    && !error.contains("database"),
2822                "approval validation ran after external setup: {error}"
2823            );
2824        }
2825    }
2826
2827    #[test]
2828    fn adoption_retry_completes_either_exact_orphan_direction() {
2829        for orphan in ["genesis", "bridge"] {
2830            let (directory, workspace) = adoption_workspace();
2831            let legacy = write_legacy_fixture(directory.path());
2832            let prepared = prepare_archive_adoption(&workspace, &legacy, "0000_archive_frontier")
2833                .expect("adoption prepares");
2834            let migration_directory = workspace
2835                .ensure_migration_directory()
2836                .expect("canonical directory");
2837            match orphan {
2838                "genesis" => {
2839                    publish_authority(
2840                        migration_directory.directory(),
2841                        type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME,
2842                        prepared.reconstructed.schema_typeql().as_bytes(),
2843                    )
2844                    .expect("genesis orphan publishes");
2845                }
2846                "bridge" => {
2847                    publish_authority(
2848                        migration_directory.directory(),
2849                        &prepared.bridge_name,
2850                        &prepared.bridge_bytes,
2851                    )
2852                    .expect("bridge orphan publishes");
2853                }
2854                _ => unreachable!(),
2855            }
2856
2857            publish_prepared_adoption(&workspace, &migration_directory, &prepared)
2858                .expect("adoption retry completes the exact orphan");
2859            workspace
2860                .discover_migrations_in(&migration_directory)
2861                .expect("completed adoption pair discovers");
2862            assert!(
2863                migration_directory
2864                    .display_path()
2865                    .join(type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME)
2866                    .is_file()
2867            );
2868            assert!(
2869                migration_directory
2870                    .display_path()
2871                    .join(&prepared.bridge_name)
2872                    .is_file()
2873            );
2874        }
2875    }
2876
2877    #[test]
2878    fn legacy_history_race_after_bridge_rolls_back_new_publication() {
2879        let (directory, workspace) = adoption_workspace();
2880        let legacy = write_legacy_fixture(directory.path());
2881        let prepared = prepare_archive_adoption(&workspace, &legacy, "0000_archive_frontier")
2882            .expect("adoption prepares");
2883        let migration_directory = workspace
2884            .ensure_migration_directory()
2885            .expect("canonical directory");
2886        let legacy_source = legacy.join("0001_initial.py");
2887
2888        let error = publish_prepared_adoption_with_after_bridge(
2889            &workspace,
2890            &migration_directory,
2891            &prepared,
2892            || {
2893                fs::write(
2894                    &legacy_source,
2895                    "class Migration:\n    operations = ['changed']\n",
2896                )
2897                .expect("race mutation writes");
2898            },
2899        )
2900        .expect_err("legacy authority race aborts pair publication");
2901        assert!(error.contains("changed"), "{error}");
2902        assert!(
2903            !migration_directory
2904                .display_path()
2905                .join(&prepared.bridge_name)
2906                .exists(),
2907            "new bridge is rolled back"
2908        );
2909        assert!(
2910            !migration_directory
2911                .display_path()
2912                .join(type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME)
2913                .exists(),
2914            "genesis is never published after the race"
2915        );
2916    }
2917}