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` run without any network I/O. `migration apply`, `migration
5//! verify`, and `migration adopt` connect through one named workspace
6//! environment: credentials stay symbolic environment references resolved
7//! only at command time, and application requires the environment's
8//! explicit `migrate: true` opt-in.
9//!
10//! The crate is a library plus a thin binary so the exact same command
11//! surface ships both as the standalone `type-bridge` executable and
12//! in-process inside the Python wheel via [`run_cli`].
13
14use std::collections::BTreeSet;
15use std::ffi::OsString;
16use std::fs;
17use std::path::{Component, Path, PathBuf};
18
19use clap::{Parser, Subcommand};
20#[cfg(test)]
21use type_bridge_schema::SystemSchemaSourceService;
22use type_bridge_schema_migration::MigrationGenerationOutcome;
23use type_bridge_schema_migration_typedb::execution_capability_vocabulary;
24use type_bridge_workspace::{
25    ConfigOrigin, ExtensionRegistryService, ExtensionRequirement, SecretReference,
26    SecretReferenceService, TypeBridgeConfigSpec, TypeBridgeWorkspace, TypeBridgeWorkspaceServices,
27    WorkspaceDirectoryAuthority, WorkspaceEnvironment, WorkspaceRoot, WorkspaceServiceError,
28    WorkspaceTransportPolicy,
29};
30
31#[derive(Parser)]
32#[command(
33    name = "type-bridge",
34    version,
35    about = "TypeBridge V2 workspace commands"
36)]
37struct Cli {
38    /// Path to the workspace manifest.
39    #[arg(long, global = true, default_value = "typebridge.yaml")]
40    manifest: PathBuf,
41    #[command(subcommand)]
42    command: Command,
43}
44
45#[derive(Subcommand)]
46enum Command {
47    /// Schema-source commands.
48    Schema {
49        #[command(subcommand)]
50        command: SchemaCommand,
51    },
52    /// Canonical migration commands.
53    Migration {
54        #[command(subcommand)]
55        command: MigrationCommand,
56    },
57}
58
59#[derive(Subcommand)]
60enum SchemaCommand {
61    /// Parse and resolve the schema sources without network I/O.
62    Check,
63    /// Generate the configured binding projections from the canonical schema.
64    Generate,
65    /// Export canonical declared-schema bytes for V2 executors.
66    ExportDeclared {
67        /// Workspace-relative destination for the canonical JSON artifact.
68        #[arg(long, default_value = "declared-schema.json")]
69        output: PathBuf,
70    },
71}
72
73#[derive(Subcommand)]
74enum MigrationCommand {
75    /// Author the next canonical migration toward the schema sources.
76    Make {
77        /// Descriptive migration name; the ordinal prefix is allocated.
78        #[arg(long)]
79        name: String,
80    },
81    /// Order the committed chain and report each manifest's safety class.
82    Plan,
83    /// Apply the committed chain to one named environment.
84    Apply {
85        /// The manifest environment to apply against.
86        #[arg(long)]
87        environment: String,
88        /// Approve one destructive migration by compound id (app/name).
89        #[arg(long = "approve")]
90        approvals: Vec<String>,
91    },
92    /// Verify the migration state triad against one named environment.
93    Verify {
94        /// The manifest environment to verify against.
95        #[arg(long)]
96        environment: String,
97    },
98    /// Adopt a completed legacy (v1) history as the canonical genesis.
99    Adopt {
100        /// The manifest environment holding the migrated v1 database.
101        #[arg(long)]
102        environment: String,
103        /// Directory containing the completed legacy migration files.
104        #[arg(long)]
105        legacy_directory: PathBuf,
106        /// Migration name recorded for the zero-operation bridge manifest.
107        #[arg(long, default_value = "0000_legacy_frontier")]
108        name: String,
109    },
110}
111
112/// Symbolic secret references stay unresolved during offline commands.
113struct DeferSecrets;
114
115impl SecretReferenceService for DeferSecrets {
116    fn validate_reference(
117        &self,
118        _reference: &SecretReference,
119    ) -> Result<(), WorkspaceServiceError> {
120        Ok(())
121    }
122}
123
124/// No extension handlers ship with the CLI yet; requirements fail closed.
125struct NoExtensions;
126
127impl ExtensionRegistryService for NoExtensions {
128    fn validate_requirement(
129        &self,
130        _requirement: &ExtensionRequirement,
131    ) -> Result<(), WorkspaceServiceError> {
132        Err(WorkspaceServiceError::new(
133            "extension_handlers_unavailable_in_cli",
134        ))
135    }
136}
137
138/// Run the CLI over process-style arguments (`argv[0]` included).
139///
140/// Returns the process exit code. All output goes to the process stdout
141/// and stderr exactly as the standalone binary would print it: `--help`
142/// and `--version` exit 0, argument errors exit 2, command failures
143/// print `error: ...` and exit 1.
144pub fn run_cli<I, T>(arguments: I) -> i32
145where
146    I: IntoIterator<Item = T>,
147    T: Into<OsString> + Clone,
148{
149    let cli = match Cli::try_parse_from(arguments) {
150        Ok(cli) => cli,
151        Err(error) => {
152            let _ = error.print();
153            return if error.use_stderr() { 2 } else { 0 };
154        }
155    };
156    match run(&cli) {
157        Ok(()) => 0,
158        Err(message) => {
159            eprintln!("error: {message}");
160            1
161        }
162    }
163}
164
165fn run(cli: &Cli) -> Result<(), String> {
166    let workspace = load_workspace(&cli.manifest)?;
167    match &cli.command {
168        Command::Schema {
169            command: SchemaCommand::Check,
170        } => {
171            println!(
172                "schema sources are valid\n  declared identity: {}\n  managed semantics: {}",
173                workspace
174                    .declared_schema()
175                    .declared_identity_fingerprint()
176                    .as_fingerprint()
177                    .digest()
178                    .to_hex(),
179                workspace
180                    .managed_state()
181                    .managed_semantic_schema()
182                    .as_fingerprint()
183                    .digest()
184                    .to_hex(),
185            );
186            Ok(())
187        }
188        Command::Schema {
189            command: SchemaCommand::Generate,
190        } => run_schema_generate(&workspace),
191        Command::Schema {
192            command: SchemaCommand::ExportDeclared { output },
193        } => run_schema_export_declared(&workspace, output),
194        Command::Migration { command } => match command {
195            MigrationCommand::Make { name } => {
196                let directory = workspace.open_migration_directory().map_err(display)?;
197                match workspace
198                    .migration_make_in(&directory, name)
199                    .map_err(display)?
200                {
201                    MigrationGenerationOutcome::UpToDate => {
202                        println!("history already reaches the desired schema");
203                    }
204                    MigrationGenerationOutcome::Generated(generated) => {
205                        workspace
206                            .write_generated_migration_in(&directory, &generated)
207                            .map_err(display)?;
208                        let path = directory.display_path().join(generated.file_name());
209                        println!(
210                            "wrote {}\n  safety: {:?}\n  preview: {}",
211                            path.display(),
212                            generated.manifest().safety(),
213                            path.with_file_name(generated.preview_file_name()).display(),
214                        );
215                    }
216                }
217                Ok(())
218            }
219            MigrationCommand::Apply {
220                environment,
221                approvals,
222            } => run_connected(
223                &workspace,
224                environment,
225                ConnectedAction::Apply {
226                    approvals: approvals.clone(),
227                },
228            ),
229            MigrationCommand::Verify { environment } => {
230                run_connected(&workspace, environment, ConnectedAction::Verify)
231            }
232            MigrationCommand::Adopt {
233                environment,
234                legacy_directory,
235                name,
236            } => run_connected(
237                &workspace,
238                environment,
239                ConnectedAction::Adopt {
240                    legacy_directory: legacy_directory.clone(),
241                    name: name.clone(),
242                },
243            ),
244            MigrationCommand::Plan => {
245                let directory = workspace.open_migration_directory().map_err(display)?;
246                let plan = workspace
247                    .migration_plan_in(&directory, &BTreeSet::new())
248                    .map_err(display)?;
249                if plan.is_empty() {
250                    println!("no committed migrations");
251                    return Ok(());
252                }
253                for entry in plan {
254                    println!(
255                        "{}/{}  safety={:?}  reversible={}",
256                        entry.id().app_label().as_str(),
257                        entry.id().name().as_str(),
258                        entry.safety(),
259                        entry.reversible(),
260                    );
261                }
262                Ok(())
263            }
264        },
265    }
266}
267
268fn load_workspace(manifest: &PathBuf) -> Result<TypeBridgeWorkspace, String> {
269    let manifest = fs::canonicalize(manifest)
270        .map_err(|error| format!("cannot resolve {}: {error}", manifest.display()))?;
271    let root = manifest
272        .parent()
273        .ok_or_else(|| "workspace manifest has no parent directory".to_owned())?;
274    let file_name = manifest
275        .file_name()
276        .and_then(|name| name.to_str())
277        .ok_or_else(|| "workspace manifest has no UTF-8 file name".to_owned())?;
278    let root = WorkspaceRoot::new(root).map_err(display)?;
279    let source = WorkspaceDirectoryAuthority::open(root.clone()).map_err(display)?;
280    let origin = ConfigOrigin::new(root, file_name, "type-bridge cli").map_err(display)?;
281    // Read at most the canonical document ceiling plus one byte: an
282    // oversized manifest fails with a stable message before its full
283    // content is ever allocated.
284    let limit = type_bridge_contract::limits::MAX_CANONICAL_BYTES;
285    let captured = source
286        .capture_relative_file(Path::new(file_name), limit)
287        .map_err(|error| format!("cannot read {}: {error}", manifest.display()))?;
288    let bytes = captured.bytes();
289    if bytes.len() > limit {
290        return Err(format!(
291            "{} exceeds the 16 MiB manifest ceiling",
292            manifest.display()
293        ));
294    }
295    let located = TypeBridgeConfigSpec::from_yaml_bytes(bytes, origin).map_err(display)?;
296
297    let available = execution_capability_vocabulary().map_err(display)?;
298    let secrets = DeferSecrets;
299    let extensions = NoExtensions;
300    let services = TypeBridgeWorkspaceServices::new(&source, &secrets, &extensions, &available);
301    // Services borrow locally, so the workspace is constructed in this scope.
302    TypeBridgeWorkspace::from_located_config(located, &services).map_err(display)
303}
304
305fn display(error: impl std::fmt::Display) -> String {
306    error.to_string()
307}
308
309/// Render one secure lifecycle failure after credentials have been resolved.
310///
311/// Provider errors are allowed to echo request metadata, including
312/// credentials. Retain only the runtime's structurally credential-safe
313/// TLS/version projection; every other error collapses to an operation code
314/// and drops its source.
315fn sanitize_connected_error(
316    context: String,
317    code: &'static str,
318    error: type_bridge_orm::SecureConnectError,
319) -> String {
320    match error.credential_safe_diagnostic() {
321        Some(diagnostic) => format!("{context}: {diagnostic}"),
322        None => format!("{context} [{code}]; inspect provider logs"),
323    }
324}
325
326/// Schema export happens after credentials have been resolved, so no raw ORM
327/// error or source chain may cross the CLI boundary.
328fn sanitize_schema_export_error(_error: type_bridge_orm::OrmError) -> String {
329    "cannot export the managed schema [typedb_schema_export_failed]; inspect provider logs"
330        .to_owned()
331}
332
333/// Render a non-success migration outcome without exposing diagnostic details.
334///
335/// TypeDB adapters retain provider text in `Diagnostic` details for trusted
336/// programmatic inspection. Its `Display` surface intentionally omits those
337/// details, while derived `Debug` includes them, so every post-credential CLI
338/// path must use this closed projection.
339fn sanitize_migration_execution_outcome(
340    context: &str,
341    outcome: type_bridge_schema_migration::MigrationExecutionOutcome,
342) -> String {
343    use type_bridge_schema_migration::{
344        MigrationExecutionOutcome as Outcome, MigrationExecutionPosition as Position,
345    };
346
347    let render_position = |position| match position {
348        Position::TransactionGroup(ordinal) => format!("transaction group {ordinal}"),
349        Position::ManifestCheckpoint => "manifest checkpoint".to_owned(),
350    };
351    match outcome {
352        Outcome::Applied => format!("{context}: applied"),
353        Outcome::RetrySafe {
354            migration_id,
355            position,
356            diagnostic,
357        } => format!(
358            "{context}: retry-safe at {}/{} ({position}): {diagnostic}",
359            migration_id.app_label().as_str(),
360            migration_id.name().as_str(),
361            position = render_position(position),
362        ),
363        Outcome::RequiresExplicitRecovery {
364            migration_id,
365            position,
366            diagnostic,
367        } => format!(
368            "{context}: explicit recovery required at {}/{} ({position}): {diagnostic}",
369            migration_id.app_label().as_str(),
370            migration_id.name().as_str(),
371            position = render_position(position),
372        ),
373    }
374}
375
376/// Generate every configured binding projection from the canonical schema.
377///
378/// The resolved workspace schema is projected per configured target with
379/// each shipped emitter's handler and code-resource evidence — the same
380/// path the codegen acceptance fixtures pin — and emitted
381/// deterministically. Files land under the confined output directories
382/// through same-directory temporary files renamed into place; files not
383/// produced by the emitter are never touched or deleted.
384fn run_schema_generate(workspace: &TypeBridgeWorkspace) -> Result<(), String> {
385    use type_bridge_contract::projection::{BindingTarget, ProjectionConfig};
386    use type_bridge_schema::project;
387    use type_bridge_schema_codegen::{PythonEmitter, RustEmitter, TypeScriptEmitter};
388
389    let outputs = workspace.config().outputs();
390    if outputs.is_empty() {
391        return Err(
392            "no binding outputs configured; add bindings.<target>.output to the manifest".into(),
393        );
394    }
395
396    let resolved = workspace.resolved_schema();
397
398    for (target, directory) in outputs {
399        let package = match target {
400            BindingTarget::Python => {
401                let emitter = PythonEmitter::new();
402                let projection = project(
403                    resolved,
404                    BindingTarget::Python,
405                    &ProjectionConfig::python(),
406                    &emitter.generator_handlers(),
407                    &emitter.code_resources().map_err(display)?,
408                )
409                .map_err(display)?;
410                emitter.emit(&projection)
411            }
412            BindingTarget::TypeScript => {
413                let emitter = TypeScriptEmitter::new();
414                let projection = project(
415                    resolved,
416                    BindingTarget::TypeScript,
417                    &ProjectionConfig::typescript(),
418                    &emitter.generator_handlers(),
419                    &emitter.code_resources().map_err(display)?,
420                )
421                .map_err(display)?;
422                emitter.emit(&projection)
423            }
424            BindingTarget::Rust => {
425                let emitter = RustEmitter::new();
426                let projection = project(
427                    resolved,
428                    BindingTarget::Rust,
429                    &ProjectionConfig::rust(),
430                    &emitter.generator_handlers(),
431                    &emitter.code_resources().map_err(display)?,
432                )
433                .map_err(display)?;
434                emitter.emit_with_declared_schema(&projection, workspace.declared_schema())
435            }
436        }
437        .map_err(display)?;
438
439        let workspace_root = workspace.output_root()?;
440        let root = workspace_root.open_beneath(directory.as_path())?;
441        for (path, bytes) in package.files() {
442            let relative = std::path::Path::new(path);
443            validate_generated_relative_path(relative)?;
444            let parent = root.open_beneath(
445                relative
446                    .parent()
447                    .unwrap_or_else(|| std::path::Path::new("")),
448            )?;
449            let file_name = relative
450                .file_name()
451                .ok_or_else(|| format!("generated path {path:?} has no file name"))?;
452            parent.write_atomic(file_name, bytes)?;
453        }
454        println!(
455            "generated {} file(s) for {} into {}",
456            package.files().len(),
457            match target {
458                BindingTarget::Python => "python",
459                BindingTarget::TypeScript => "typescript",
460                BindingTarget::Rust => "rust",
461            },
462            root.display_path().display(),
463        );
464    }
465    Ok(())
466}
467
468/// Export the same canonical declared bytes consumed by the V2 server.
469fn run_schema_export_declared(
470    workspace: &TypeBridgeWorkspace,
471    output: &Path,
472) -> Result<(), String> {
473    use type_bridge_contract::schema::encode_declared_schema;
474
475    validate_declared_output_path(output)?;
476    let root = workspace.output_root()?;
477    let parent = root.open_beneath(output.parent().unwrap_or_else(|| std::path::Path::new("")))?;
478    let file_name = output
479        .file_name()
480        .ok_or_else(|| "declared-schema output has no file name".to_owned())?;
481    let destination = parent.display_path().join(file_name);
482    let bytes = encode_declared_schema(workspace.declared_schema()).map_err(display)?;
483    parent.write_atomic(file_name, &bytes)?;
484    println!(
485        "wrote canonical declared schema to {}\n  declared identity: {}",
486        destination.display(),
487        workspace
488            .declared_schema()
489            .declared_identity_fingerprint()
490            .as_fingerprint()
491            .digest()
492            .to_hex(),
493    );
494    Ok(())
495}
496
497fn validate_declared_output_path(output: &Path) -> Result<(), String> {
498    let Some(portable) = output.to_str() else {
499        return Err("declared-schema output must be valid UTF-8".into());
500    };
501    let invalid_spelling = portable.is_empty()
502        || portable.contains(['\\', ':', '\0'])
503        || portable.bytes().any(|byte| byte.is_ascii_control())
504        || portable
505            .split('/')
506            .any(|segment| segment.is_empty() || matches!(segment, "." | ".."));
507    let invalid_components = output.is_absolute()
508        || output
509            .components()
510            .any(|component| !matches!(component, Component::Normal(_)));
511    if invalid_spelling || invalid_components {
512        return Err("declared-schema output must be a confined portable workspace path".into());
513    }
514    if output.extension().and_then(|extension| extension.to_str()) != Some("json") {
515        return Err("declared-schema output must end in lowercase .json".into());
516    }
517    Ok(())
518}
519
520fn validate_generated_relative_path(path: &std::path::Path) -> Result<(), String> {
521    if path.as_os_str().is_empty()
522        || path
523            .components()
524            .any(|component| !matches!(component, std::path::Component::Normal(_)))
525    {
526        return Err(format!(
527            "generated output path {:?} is not a confined relative file",
528            path
529        ));
530    }
531    Ok(())
532}
533
534enum ConnectedAction {
535    Apply {
536        approvals: Vec<String>,
537    },
538    Verify,
539    Adopt {
540        legacy_directory: PathBuf,
541        name: String,
542    },
543}
544
545fn secure_connect_options(
546    environment: &WorkspaceEnvironment,
547) -> type_bridge_orm::SecureConnectOptions {
548    let tls_mode = match environment.transport_policy() {
549        WorkspaceTransportPolicy::Disabled => type_bridge_orm::TlsMode::Disabled,
550        WorkspaceTransportPolicy::NativeRoots => type_bridge_orm::TlsMode::NativeRoots,
551        WorkspaceTransportPolicy::CustomRootCa(root_ca) => {
552            type_bridge_orm::TlsMode::CustomRootCa(root_ca.as_path().to_path_buf())
553        }
554    };
555    let mut options = type_bridge_orm::SecureConnectOptions {
556        tls_mode,
557        ..type_bridge_orm::SecureConnectOptions::default()
558    };
559    if let Some(port) = environment.http_port() {
560        options.http_port = port;
561    }
562    options
563}
564
565fn preflight_secure_connect_options(
566    workspace: &TypeBridgeWorkspace,
567    environment_name: &str,
568) -> Result<type_bridge_orm::PreparedSecureConnectOptions, String> {
569    let environment = workspace
570        .config()
571        .environment(environment_name)
572        .ok_or_else(|| {
573            format!("environment {environment_name:?} is not owned by this workspace")
574        })?;
575    let options = secure_connect_options(environment);
576    match workspace
577        .capture_environment_custom_root_ca(environment_name)
578        .map_err(display)?
579    {
580        Some(bytes) => options
581            .prepare_transport_from_captured_custom_root(bytes)
582            .map_err(display),
583        None => options.prepare_transport().map_err(display),
584    }
585}
586
587fn run_connected(
588    workspace: &TypeBridgeWorkspace,
589    environment: &str,
590    action: ConnectedAction,
591) -> Result<(), String> {
592    let runtime = tokio::runtime::Runtime::new()
593        .map_err(|error| format!("cannot start the async runtime: {error}"))?;
594    runtime.block_on(run_connected_async(workspace, environment, action))
595}
596
597async fn run_connected_async(
598    workspace: &TypeBridgeWorkspace,
599    environment_name: &str,
600    action: ConnectedAction,
601) -> Result<(), String> {
602    let config = workspace.config();
603    let Some(environment) = config.environment(environment_name) else {
604        let known = config
605            .environments()
606            .keys()
607            .cloned()
608            .collect::<Vec<_>>()
609            .join(", ");
610        return Err(format!(
611            "unknown environment {environment_name:?}; the manifest declares: [{known}]"
612        ));
613    };
614    if matches!(
615        &action,
616        ConnectedAction::Apply { .. } | ConnectedAction::Adopt { .. }
617    ) && !environment.migrate()
618    {
619        return Err(format!(
620            "environment {environment_name:?} is not opted into migration \
621             application; set `migrate: true` in the manifest to allow it"
622        ));
623    }
624    environment
625        .requirements()
626        .ensure_supported_by(&execution_capability_vocabulary().map_err(display)?)
627        .map_err(display)?;
628
629    // Validate the name and capture one immutable legacy-history authority
630    // before creating the canonical directory or resolving credentials.
631    let prepared_adoption = match &action {
632        ConnectedAction::Adopt {
633            legacy_directory,
634            name,
635        } => Some(prepare_legacy_adoption(workspace, legacy_directory, name)?),
636        ConnectedAction::Apply { .. } | ConnectedAction::Verify => None,
637    };
638
639    // Retain one descriptor-backed authority for the whole connected action.
640    // Adoption alone may create missing real directory components; apply and
641    // verify remain fail-closed and non-creating here.
642    let migration_directory = if matches!(&action, ConnectedAction::Adopt { .. }) {
643        workspace.ensure_migration_directory().map_err(display)?
644    } else {
645        workspace.open_migration_directory().map_err(display)?
646    };
647    // Ordinary connected operations reject an incomplete adoption pair before
648    // credentials, network I/O, or database creation. Adoption itself is the
649    // sole recovery path permitted to observe and complete an exact orphan.
650    let ordinary_graph = if prepared_adoption.is_none() {
651        Some(
652            workspace
653                .discover_migrations_in(&migration_directory)
654                .map_err(display)?,
655        )
656    } else {
657        None
658    };
659    // Approval syntax, membership, safety, and digest binding are local
660    // authority checks. Resolve them before credentials, network I/O, or
661    // database creation; the runner re-discovers and rechecks the bound
662    // manifest at execution time.
663    let prepared_approvals = match &action {
664        ConnectedAction::Apply { approvals } => Some(bind_approvals(
665            ordinary_graph
666                .as_ref()
667                .ok_or_else(|| "internal apply history was not retained".to_owned())?,
668            approvals,
669        )?),
670        ConnectedAction::Verify | ConnectedAction::Adopt { .. } => None,
671    };
672
673    // Resolve and snapshot the complete transport policy before reading either
674    // credential. Every later lifecycle/connect call clones this prepared
675    // handle, so no custom-root path is reopened after secret resolution.
676    let options = preflight_secure_connect_options(workspace, environment_name)?;
677    let username = resolve_credential(environment.username())?;
678    let password = resolve_credential(environment.password())?;
679    let journal_name =
680        type_bridge_schema_migration_typedb::derived_journal_database_name(environment.database());
681    // `verify` is observational: it must never create the managed or
682    // journal database (a typoed environment name would otherwise
683    // materialize two databases). `adopt` requires the migrated v1 managed
684    // database to already exist — bootstrapping an empty one would
685    // guarantee a broken adoption — while its journal companion is new by
686    // definition. Only migration-gated actions may bootstrap anything.
687    let managed_requires_existing = match &action {
688        ConnectedAction::Verify => Some(
689            "`migration verify` is read-only and never creates databases \
690             — apply migrations to this environment first",
691        ),
692        ConnectedAction::Adopt { .. } => {
693            Some("`migration adopt` cutover requires the migrated v1 database to already exist")
694        }
695        ConnectedAction::Apply { .. } => None,
696    };
697    if let Some(reason) = managed_requires_existing {
698        let exists = type_bridge_orm::database_exists_prepared_secure(
699            environment.uri(),
700            environment.database(),
701            &username,
702            &password,
703            options.clone(),
704        )
705        .await
706        .map_err(|error| {
707            sanitize_connected_error(
708                format!("cannot check database {:?}", environment.database()),
709                "typedb_database_exists_failed",
710                error,
711            )
712        })?;
713        if !exists {
714            return Err(format!(
715                "database {:?} does not exist; {reason}",
716                environment.database()
717            ));
718        }
719    } else {
720        type_bridge_orm::ensure_database_exists_prepared_secure(
721            environment.uri(),
722            environment.database(),
723            &username,
724            &password,
725            options.clone(),
726        )
727        .await
728        .map_err(|error| {
729            sanitize_connected_error(
730                format!("cannot ensure database {:?}", environment.database()),
731                "typedb_database_ensure_failed",
732                error,
733            )
734        })?;
735    }
736    let managed = std::sync::Arc::new(
737        type_bridge_orm::Database::connect_prepared_secure_with_options(
738            environment.uri(),
739            environment.database(),
740            &username,
741            &password,
742            options.clone(),
743        )
744        .await
745        .map_err(|error| {
746            sanitize_connected_error(
747                "cannot connect the managed database".to_owned(),
748                "typedb_database_connect_failed",
749                error,
750            )
751        })?,
752    );
753
754    // Adoption's live-schema comparison and complete pair publication precede
755    // journal creation. Publication is bridge-first under the canonical
756    // authoring lock, rolls back files created by a failed attempt, and accepts
757    // exact orphan pieces so interrupted attempts remain adopt-only resumable.
758    let adoption_files = if let Some(prepared) = prepared_adoption.as_ref() {
759        verify_prepared_adoption_live(&managed, prepared).await?;
760        Some(publish_prepared_adoption(
761            workspace,
762            &migration_directory,
763            prepared,
764        )?)
765    } else {
766        None
767    };
768
769    if matches!(&action, ConnectedAction::Verify) {
770        let exists = type_bridge_orm::database_exists_prepared_secure(
771            environment.uri(),
772            &journal_name,
773            &username,
774            &password,
775            options.clone(),
776        )
777        .await
778        .map_err(|error| {
779            sanitize_connected_error(
780                format!("cannot check database {journal_name:?}"),
781                "typedb_database_exists_failed",
782                error,
783            )
784        })?;
785        if !exists {
786            return Err(format!(
787                "database {journal_name:?} does not exist; `migration verify` is read-only and never creates databases"
788            ));
789        }
790    } else {
791        type_bridge_orm::ensure_database_exists_prepared_secure(
792            environment.uri(),
793            &journal_name,
794            &username,
795            &password,
796            options.clone(),
797        )
798        .await
799        .map_err(|error| {
800            sanitize_connected_error(
801                format!("cannot ensure database {journal_name:?}"),
802                "typedb_database_ensure_failed",
803                error,
804            )
805        })?;
806    }
807    let journal = std::sync::Arc::new(
808        type_bridge_orm::Database::connect_prepared_secure_with_options(
809            environment.uri(),
810            &journal_name,
811            &username,
812            &password,
813            options,
814        )
815        .await
816        .map_err(|error| {
817            sanitize_connected_error(
818                "cannot connect the journal database".to_owned(),
819                "typedb_database_connect_failed",
820                error,
821            )
822        })?,
823    );
824
825    let genesis = workspace
826        .migration_genesis_in(&migration_directory)
827        .map_err(display)?;
828    let lowering = type_bridge_schema_migration::SchemaLoweringBinding::current(
829        workspace.delta_context().available_capabilities().clone(),
830    )
831    .map_err(display)?;
832    let runner = type_bridge_schema_migration_typedb::TypeDbMigrationRunner::new(
833        managed,
834        journal,
835        genesis.clone(),
836        workspace.delta_context().clone(),
837        lowering,
838        config.migration_policy().clone(),
839    );
840    let holder =
841        type_bridge_schema_migration::LeaseHolderId::new("type-bridge-cli").map_err(display)?;
842    let directory = migration_directory.directory();
843
844    match action {
845        ConnectedAction::Apply { .. } => {
846            let approvals = prepared_approvals
847                .as_deref()
848                .ok_or_else(|| "internal apply approvals were not retained".to_owned())?;
849            let outcome = runner
850                .apply_in(
851                    directory,
852                    &type_bridge_schema_migration::MigrationApplyTarget::DefaultHead,
853                    &holder,
854                    approvals,
855                )
856                .await
857                .map_err(display)?;
858            match outcome {
859                type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::UpToDate => {
860                    println!("applied ledger already reaches the committed head");
861                    Ok(())
862                }
863                type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::Executed(
864                    type_bridge_schema_migration::MigrationExecutionOutcome::Applied,
865                ) => {
866                    println!("applied the committed chain");
867                    Ok(())
868                }
869                type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::Executed(
870                    outcome,
871                ) => Err(sanitize_migration_execution_outcome(
872                    "apply did not complete",
873                    outcome,
874                )),
875            }
876        }
877        ConnectedAction::Verify => {
878            let report = runner
879                .verify_in(directory, Some(workspace.declared_schema()))
880                .await
881                .map_err(display)?;
882            if report.is_clean() {
883                println!(
884                    "migration state is coherent\n  applied frontier: {}",
885                    report
886                        .applied_frontier()
887                        .iter()
888                        .map(|id| format!("{}/{}", id.app_label().as_str(), id.name().as_str()))
889                        .collect::<Vec<_>>()
890                        .join(", "),
891                );
892                Ok(())
893            } else {
894                for finding in report.findings() {
895                    eprintln!("drift: {finding:?}");
896                }
897                Err(format!("{} drift finding(s)", report.findings().len()))
898            }
899        }
900        ConnectedAction::Adopt { .. } => {
901            let bridge_display_path = adoption_files
902                .ok_or_else(|| "internal adoption preflight state was not retained".to_owned())?;
903            let prepared = prepared_adoption
904                .as_ref()
905                .ok_or_else(|| "internal adoption authority was not retained".to_owned())?;
906            let outcome = runner
907                .import_verified_legacy_frontier_in(
908                    &prepared.history,
909                    &prepared.reconstructed,
910                    directory,
911                    &holder,
912                )
913                .await;
914            match outcome {
915                Ok(
916                    type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::UpToDate,
917                ) => {
918                    println!("legacy history is already adopted; the bridged ledger is current");
919                    Ok(())
920                }
921                Ok(
922                    type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::Executed(
923                        type_bridge_schema_migration::MigrationExecutionOutcome::Applied,
924                    ),
925                ) => {
926                    println!(
927                        "adopted the legacy history\n  genesis: {}\n  bridge: {}",
928                        migration_directory
929                            .display_path()
930                            .join(type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME)
931                            .display(),
932                        bridge_display_path.display(),
933                    );
934                    Ok(())
935                }
936                Ok(
937                    type_bridge_schema_migration_typedb::MigrationDirectoryApplyOutcome::Executed(
938                        outcome,
939                    ),
940                ) => Err(sanitize_migration_execution_outcome(
941                    "adoption checkpoint did not complete",
942                    outcome,
943                )),
944                Err(error) => Err(display(error)),
945            }
946        }
947    }
948}
949
950struct PreparedLegacyAdoption {
951    history: type_bridge_migration::LegacyAdoptionHistory,
952    reconstructed: type_bridge_migration::VerifiedLegacyHead,
953    authority: type_bridge_schema_compat::AdoptedGenesisAuthority,
954    bridge: type_bridge_schema_migration::VerifiedSchemaMigrationManifest,
955    bridge_name: String,
956    bridge_bytes: Vec<u8>,
957}
958
959/// Validate and derive every filesystem authority from one retained legacy
960/// history capture. This function performs no canonical-directory writes.
961fn prepare_legacy_adoption(
962    workspace: &TypeBridgeWorkspace,
963    legacy_directory: &std::path::Path,
964    name: &str,
965) -> Result<PreparedLegacyAdoption, String> {
966    // Validate the caller-controlled name before loading history or creating
967    // the configured canonical directory.
968    let migration_name =
969        type_bridge_contract::migration::MigrationName::new(name.to_owned()).map_err(display)?;
970    let bridge_name = format!("{}.tbmigration.json", migration_name.as_str());
971    let history =
972        type_bridge_migration::load_adoption_history(legacy_directory).map_err(|error| {
973            format!("legacy migration directory failed the checked adoption loader: {error}")
974        })?;
975    let reconstructed = type_bridge_migration::reconstruct_legacy_head(&history)
976        .map_err(|error| format!("legacy head reconstruction failed: {error}"))?;
977    let authority = type_bridge_schema_compat::parse_adopted_genesis_authority(
978        type_bridge_contract::schema::DocumentId::new("legacy-head-snapshot.typeql")
979            .map_err(display)?,
980        reconstructed.schema_typeql(),
981    )
982    .map_err(display)?;
983    let frontier = type_bridge_schema_migration_typedb::extract_legacy_frontier(history.graph())
984        .map_err(display)?;
985    let applied_set =
986        type_bridge_schema_migration_typedb::extract_legacy_applied_set_digest(history.graph())
987            .map_err(display)?;
988    let id = type_bridge_contract::migration::MigrationId::from_components(
989        type_bridge_contract::migration::MigrationAppLabel::new(
990            workspace.config().app_label().as_str().to_owned(),
991        )
992        .map_err(display)?,
993        migration_name,
994    );
995    let bridge = type_bridge_schema_migration::build_legacy_frontier_bridge(
996        id,
997        frontier,
998        applied_set,
999        authority.declared(),
1000        workspace.delta_context(),
1001    )
1002    .map_err(display)?;
1003    let bridge_bytes =
1004        type_bridge_schema_migration::encode_verified_manifest(&bridge).map_err(display)?;
1005    history
1006        .require_unchanged_head(&reconstructed)
1007        .map_err(|error| {
1008            format!("legacy migration directory changed during adoption preparation: {error}")
1009        })?;
1010    Ok(PreparedLegacyAdoption {
1011        history,
1012        reconstructed,
1013        authority,
1014        bridge,
1015        bridge_name,
1016        bridge_bytes,
1017    })
1018}
1019
1020/// Compare the live managed schema with the prepared immutable head without
1021/// using live state as publication authority.
1022async fn verify_prepared_adoption_live(
1023    managed: &type_bridge_orm::Database,
1024    prepared: &PreparedLegacyAdoption,
1025) -> Result<(), String> {
1026    let export = managed
1027        .schema_text()
1028        .await
1029        .map_err(sanitize_schema_export_error)?;
1030    prepared
1031        .history
1032        .require_unchanged_head(&prepared.reconstructed)
1033        .map_err(|error| format!("legacy adoption history changed during live export: {error}"))?;
1034    let expected_internal = type_bridge_schema_compat::released_typeql_to_declared_projection(
1035        type_bridge_contract::schema::DocumentId::new("managed-fence-schema.typeql")
1036            .map_err(display)?,
1037        type_bridge_schema_migration_typedb::MANAGED_FENCE_SCHEMA_TYPEQL,
1038    )
1039    .map_err(display)?;
1040    let live = type_bridge_schema_compat::parse_adopted_genesis_authority_with_internal(
1041        type_bridge_contract::schema::DocumentId::new("legacy-live-head.typeql")
1042            .map_err(display)?,
1043        &export,
1044        Some(&expected_internal),
1045    )
1046    .map_err(display)?;
1047    if live.legacy_identity() != prepared.authority.legacy_identity()
1048        || live.declared().declared_identity_fingerprint()
1049            != prepared
1050                .authority
1051                .declared()
1052                .declared_identity_fingerprint()
1053        || live.released_extension_identity() != prepared.authority.released_extension_identity()
1054    {
1055        return Err(
1056            "live managed schema differs from the independently verified legacy-head snapshot"
1057                .to_owned(),
1058        );
1059    }
1060    Ok(())
1061}
1062
1063/// Publish the bridge/genesis pair under the shared authoring lock.
1064///
1065/// The prospective complete graph is replay-verified before publication. The
1066/// bridge is made visible first, so ordinary readers fail closed during the
1067/// short incomplete interval. Exact pre-existing orphan pieces are retained
1068/// and completed; only files created by this attempt are rolled back.
1069fn publish_prepared_adoption(
1070    workspace: &TypeBridgeWorkspace,
1071    migration_directory: &type_bridge_workspace::MigrationDirectoryAuthority,
1072    prepared: &PreparedLegacyAdoption,
1073) -> Result<PathBuf, String> {
1074    publish_prepared_adoption_with_after_bridge(workspace, migration_directory, prepared, || {})
1075}
1076
1077fn publish_prepared_adoption_with_after_bridge<F>(
1078    workspace: &TypeBridgeWorkspace,
1079    migration_directory: &type_bridge_workspace::MigrationDirectoryAuthority,
1080    prepared: &PreparedLegacyAdoption,
1081    after_bridge: F,
1082) -> Result<PathBuf, String>
1083where
1084    F: FnOnce(),
1085{
1086    let directory = migration_directory.directory();
1087    let _lock = directory.try_acquire_authoring_lock().map_err(|error| {
1088        if error.kind() == std::io::ErrorKind::WouldBlock {
1089            "migration adoption conflicts with another canonical history publisher".to_owned()
1090        } else {
1091            format!("cannot lock canonical migration publication: {error}")
1092        }
1093    })?;
1094    let genesis_name = type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME;
1095    let genesis_bytes = prepared.reconstructed.schema_typeql().as_bytes();
1096    let mut bridge_created = false;
1097    let mut genesis_created = false;
1098    let mut after_bridge = Some(after_bridge);
1099
1100    let publication = (|| -> Result<(), String> {
1101        if let Some(existing) = read_existing_authority(directory, genesis_name)?
1102            && existing != genesis_bytes
1103        {
1104            return Err(format!(
1105                "{genesis_name} already exists but differs from the verified legacy-head snapshot"
1106            ));
1107        }
1108        let bridge_already_published =
1109            if let Some(existing) = read_existing_authority(directory, &prepared.bridge_name)? {
1110                if existing != prepared.bridge_bytes {
1111                    return Err(format!(
1112                        "{} already exists with different authority bytes",
1113                        prepared.bridge_name
1114                    ));
1115                }
1116                true
1117            } else {
1118                false
1119            };
1120
1121        let (current, evidence) =
1122            type_bridge_schema_migration::discover_verified_migration_chain_with_evidence_in(
1123                directory,
1124                prepared.authority.declared(),
1125                workspace.delta_context(),
1126            )
1127            .map_err(display)?;
1128        let prospective = if current.manifest(prepared.bridge.id()).is_some() {
1129            current
1130        } else {
1131            let manifests = current
1132                .manifests()
1133                .map(|(_, manifest)| manifest.clone())
1134                .chain(std::iter::once(prepared.bridge.clone()))
1135                .collect::<Vec<_>>();
1136            type_bridge_schema_migration::MigrationHistoryGraph::from_verified(manifests)
1137                .map_err(display)?
1138        };
1139        type_bridge_schema_migration::require_adoption_authority_pair(&prospective, true)
1140            .map_err(display)?;
1141        evidence.require_unchanged(directory).map_err(display)?;
1142        prepared
1143            .history
1144            .require_unchanged_head(&prepared.reconstructed)
1145            .map_err(|error| {
1146                format!("legacy adoption history changed before pair publication: {error}")
1147            })?;
1148
1149        if !bridge_already_published {
1150            bridge_created =
1151                publish_authority(directory, &prepared.bridge_name, &prepared.bridge_bytes)?;
1152        }
1153        if let Some(after_bridge) = after_bridge.take() {
1154            after_bridge();
1155        }
1156        prepared
1157            .history
1158            .require_unchanged_head(&prepared.reconstructed)
1159            .map_err(|error| {
1160                format!("legacy adoption history changed before genesis publication: {error}")
1161            })?;
1162        genesis_created = publish_authority(directory, genesis_name, genesis_bytes)?;
1163        prepared
1164            .history
1165            .require_unchanged_head(&prepared.reconstructed)
1166            .map_err(|error| {
1167                format!("legacy adoption history changed after pair publication: {error}")
1168            })?;
1169        workspace
1170            .discover_migrations_in(migration_directory)
1171            .map_err(display)?;
1172        Ok(())
1173    })();
1174
1175    if let Err(error) = publication {
1176        return Err(rollback_adoption_publication(
1177            directory,
1178            &prepared.bridge_name,
1179            bridge_created,
1180            genesis_created,
1181            error,
1182        ));
1183    }
1184    Ok(migration_directory
1185        .display_path()
1186        .join(&prepared.bridge_name))
1187}
1188
1189fn rollback_adoption_publication(
1190    directory: &type_bridge_schema_migration::MigrationDirectory,
1191    bridge_name: &str,
1192    bridge_created: bool,
1193    genesis_created: bool,
1194    primary: String,
1195) -> String {
1196    let mut cleanup_errors = Vec::new();
1197    if genesis_created
1198        && let Err(error) =
1199            directory.remove_file(type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME.as_ref())
1200    {
1201        cleanup_errors.push(format!("cannot remove newly published genesis: {error}"));
1202    }
1203    if bridge_created && let Err(error) = directory.remove_file(bridge_name.as_ref()) {
1204        cleanup_errors.push(format!("cannot remove newly published bridge: {error}"));
1205    }
1206    if (bridge_created || genesis_created)
1207        && let Err(error) = directory.sync_all()
1208    {
1209        cleanup_errors.push(format!("cannot flush adoption rollback: {error}"));
1210    }
1211    if cleanup_errors.is_empty() {
1212        primary
1213    } else {
1214        format!(
1215            "{primary}; adoption publication rollback failed: {}",
1216            cleanup_errors.join("; ")
1217        )
1218    }
1219}
1220
1221/// Publish immutable authority from a unique, flushed same-directory temp.
1222///
1223/// Hard-link publication is atomic and no-replace. An existing final name is
1224/// accepted only when its bounded bytes are identical, allowing a retry to
1225/// recover after publication succeeded but the caller did not observe it. In
1226/// particular, a directory-sync error may be reported after the final link is
1227/// already durable; that exact orphan is intentionally left for the same
1228/// adoption command to recognize and complete on retry.
1229fn publish_authority(
1230    directory: &type_bridge_schema_migration::MigrationDirectory,
1231    name: &str,
1232    bytes: &[u8],
1233) -> Result<bool, String> {
1234    use std::io::Write;
1235    if let Some(existing) = read_existing_authority(directory, name)? {
1236        if existing == bytes {
1237            return Ok(false);
1238        }
1239        return Err(format!(
1240            "{name} already exists with different authority bytes"
1241        ));
1242    }
1243    let mut temporary = None;
1244    for attempt in 0..128_u64 {
1245        let candidate = unique_authority_temporary_name(name, attempt);
1246        match directory.create_new(candidate.as_ref()) {
1247            Ok(mut file) => {
1248                if let Err(error) = file.write_all(bytes).and_then(|()| file.sync_all()) {
1249                    let _ = directory.remove_file(candidate.as_ref());
1250                    return Err(format!("cannot write {candidate}: {error}"));
1251                }
1252                temporary = Some(candidate);
1253                break;
1254            }
1255            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
1256            Err(error) => {
1257                return Err(format!("cannot create {candidate}: {error}"));
1258            }
1259        }
1260    }
1261    let temporary = temporary.ok_or_else(|| {
1262        format!("cannot allocate a unique temporary authority file beside {name}")
1263    })?;
1264    let publication = directory.hard_link(temporary.as_ref(), name.as_ref());
1265    match publication {
1266        Ok(()) => {
1267            if let Err(error) = directory.sync_all() {
1268                let _ = directory.remove_file(temporary.as_ref());
1269                return Err(format!("cannot flush migration directory: {error}"));
1270            }
1271            let _ = directory.remove_file(temporary.as_ref());
1272            directory
1273                .sync_all()
1274                .map_err(|error| format!("cannot flush migration directory: {error}"))?;
1275            Ok(true)
1276        }
1277        Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
1278            let _ = directory.remove_file(temporary.as_ref());
1279            let existing = read_existing_authority(directory, name)?
1280                .ok_or_else(|| format!("{name} disappeared during no-replace publication"))?;
1281            if existing == bytes {
1282                Ok(false)
1283            } else {
1284                Err(format!(
1285                    "{name} was concurrently published with different authority bytes"
1286                ))
1287            }
1288        }
1289        Err(error) => {
1290            let _ = directory.remove_file(temporary.as_ref());
1291            Err(format!("cannot publish {name}: {error}"))
1292        }
1293    }
1294}
1295
1296fn read_existing_authority(
1297    directory: &type_bridge_schema_migration::MigrationDirectory,
1298    name: &str,
1299) -> Result<Option<Vec<u8>>, String> {
1300    use std::io::Read;
1301    let file = match directory.open_regular_readonly(name.as_ref()) {
1302        Ok(file) => file,
1303        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1304        Err(error) => return Err(format!("cannot read {name}: {error}")),
1305    };
1306    let limit = type_bridge_contract::limits::MAX_CANONICAL_BYTES;
1307    let mut bytes = Vec::new();
1308    file.take(u64::try_from(limit).unwrap_or(u64::MAX).saturating_add(1))
1309        .read_to_end(&mut bytes)
1310        .map_err(|error| format!("cannot read {name}: {error}"))?;
1311    if bytes.len() > limit {
1312        return Err(format!("{name} exceeds the 16 MiB authority ceiling"));
1313    }
1314    Ok(Some(bytes))
1315}
1316
1317fn unique_authority_temporary_name(name: &str, attempt: u64) -> String {
1318    use std::sync::atomic::{AtomicU64, Ordering};
1319    static NEXT_AUTHORITY_TEMPORARY: AtomicU64 = AtomicU64::new(1);
1320    let nonce = NEXT_AUTHORITY_TEMPORARY.fetch_add(1, Ordering::Relaxed);
1321    format!(".{name}.{}.{}.{}.tmp", std::process::id(), nonce, attempt)
1322}
1323
1324fn resolve_credential(
1325    reference: &type_bridge_workspace::SecretReference,
1326) -> Result<String, String> {
1327    std::env::var(reference.environment_variable()).map_err(|_| {
1328        format!(
1329            "credential environment variable {:?} is not set",
1330            reference.environment_variable()
1331        )
1332    })
1333}
1334
1335#[cfg(test)]
1336mod credential_error_redaction_tests {
1337    use super::*;
1338    use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
1339    use type_bridge_typedb_runtime::RuntimeError;
1340
1341    const PROVIDER_TEXT: &str =
1342        "TB_ADDRESS_SECRET TB_USERNAME_SECRET TB_PASSWORD_SECRET TB_PROVIDER_SECRET";
1343    const SECRETS: [&str; 4] = [
1344        "TB_ADDRESS_SECRET",
1345        "TB_USERNAME_SECRET",
1346        "TB_PASSWORD_SECRET",
1347        "TB_PROVIDER_SECRET",
1348    ];
1349
1350    fn hostile_secure_error() -> type_bridge_orm::SecureConnectError {
1351        type_bridge_orm::SecureConnectError::Runtime(RuntimeError::Connection(
1352            PROVIDER_TEXT.to_owned(),
1353        ))
1354    }
1355
1356    #[test]
1357    fn connected_lifecycle_contexts_drop_hostile_provider_text() {
1358        for (context, code) in [
1359            (
1360                "cannot check database \"managed\"",
1361                "typedb_database_exists_failed",
1362            ),
1363            (
1364                "cannot ensure database \"managed\"",
1365                "typedb_database_ensure_failed",
1366            ),
1367            (
1368                "cannot connect the managed database",
1369                "typedb_database_connect_failed",
1370            ),
1371        ] {
1372            let sanitized =
1373                sanitize_connected_error(context.to_owned(), code, hostile_secure_error());
1374            let rendered = format!("{sanitized}\n{sanitized:?}");
1375            for secret in SECRETS {
1376                assert!(!rendered.contains(secret), "{secret}: {rendered}");
1377            }
1378            assert!(rendered.contains(context), "{rendered}");
1379            assert!(rendered.contains(code), "{rendered}");
1380        }
1381    }
1382
1383    #[test]
1384    fn connected_lifecycle_preserves_only_typed_safe_diagnostics() {
1385        let sanitized = sanitize_connected_error(
1386            "cannot connect the managed database".to_owned(),
1387            "typedb_database_connect_failed",
1388            type_bridge_orm::SecureConnectError::DriverTlsConfiguration { band: 9 },
1389        );
1390        assert!(
1391            sanitized.contains("tls_driver_lowering_failed"),
1392            "{sanitized}"
1393        );
1394        assert!(sanitized.contains("driver band 9"), "{sanitized}");
1395        assert!(
1396            !sanitized.contains("typedb_database_connect_failed"),
1397            "{sanitized}"
1398        );
1399    }
1400
1401    #[test]
1402    fn schema_export_drops_hostile_orm_text_and_source() {
1403        let sanitized = sanitize_schema_export_error(type_bridge_orm::OrmError::Connection(
1404            PROVIDER_TEXT.into(),
1405        ));
1406        let rendered = format!("{sanitized}\n{sanitized:?}");
1407        for secret in SECRETS {
1408            assert!(!rendered.contains(secret), "{secret}: {rendered}");
1409        }
1410        assert!(
1411            rendered.contains("typedb_schema_export_failed"),
1412            "{rendered}"
1413        );
1414    }
1415
1416    #[test]
1417    fn migration_runner_display_omits_provider_details() {
1418        let diagnostic = Diagnostic::new(
1419            DiagnosticCategory::InvalidContract,
1420            DiagnosticCode::new("migration_provider_test_failed").expect("static code"),
1421            "migration provider operation failed",
1422        )
1423        .with_detail("provider", PROVIDER_TEXT);
1424        let error = type_bridge_schema_migration_typedb::MigrationDirectoryApplyError::Diagnostic(
1425            diagnostic,
1426        );
1427
1428        let rendered = display(error);
1429        for secret in SECRETS {
1430            assert!(!rendered.contains(secret), "{secret}: {rendered}");
1431        }
1432        assert!(
1433            rendered.contains("migration_provider_test_failed"),
1434            "{rendered}"
1435        );
1436    }
1437
1438    #[test]
1439    fn migration_outcome_projection_omits_provider_details_for_apply_and_adopt() {
1440        use type_bridge_contract::migration::MigrationId;
1441        use type_bridge_schema_migration::{MigrationExecutionOutcome, MigrationExecutionPosition};
1442
1443        let diagnostic = || {
1444            Diagnostic::new(
1445                DiagnosticCategory::InvalidContract,
1446                DiagnosticCode::new("migration_provider_test_failed").expect("static code"),
1447                "migration provider operation failed",
1448            )
1449            .with_detail("provider", PROVIDER_TEXT)
1450        };
1451        for context in [
1452            "apply did not complete",
1453            "adoption checkpoint did not complete",
1454        ] {
1455            for (outcome, expected_state, expected_position) in [
1456                (
1457                    MigrationExecutionOutcome::RetrySafe {
1458                        migration_id: MigrationId::new("example", "0001_initial")
1459                            .expect("migration id"),
1460                        position: MigrationExecutionPosition::TransactionGroup(7),
1461                        diagnostic: diagnostic(),
1462                    },
1463                    "retry-safe",
1464                    "transaction group 7",
1465                ),
1466                (
1467                    MigrationExecutionOutcome::RequiresExplicitRecovery {
1468                        migration_id: MigrationId::new("example", "0001_initial")
1469                            .expect("migration id"),
1470                        position: MigrationExecutionPosition::ManifestCheckpoint,
1471                        diagnostic: diagnostic(),
1472                    },
1473                    "explicit recovery required",
1474                    "manifest checkpoint",
1475                ),
1476            ] {
1477                let rendered = sanitize_migration_execution_outcome(context, outcome);
1478                for secret in SECRETS {
1479                    assert!(!rendered.contains(secret), "{secret}: {rendered}");
1480                }
1481                for expected in [
1482                    context,
1483                    expected_state,
1484                    "example/0001_initial",
1485                    expected_position,
1486                    "migration_provider_test_failed",
1487                    "migration provider operation failed",
1488                ] {
1489                    assert!(rendered.contains(expected), "{expected}: {rendered}");
1490                }
1491            }
1492        }
1493    }
1494}
1495
1496#[cfg(all(test, unix))]
1497mod output_authority_tests {
1498    use super::*;
1499    use std::os::unix::fs::symlink;
1500
1501    #[test]
1502    fn retained_output_authority_survives_component_swap_without_redirecting() {
1503        let workspace = tempfile::tempdir().expect("workspace directory");
1504        let outside = tempfile::tempdir().expect("outside directory");
1505        fs::create_dir_all(workspace.path().join("generated/python")).expect("output directory");
1506        let authority = WorkspaceDirectoryAuthority::open(
1507            WorkspaceRoot::new(fs::canonicalize(workspace.path()).expect("canonical workspace"))
1508                .expect("workspace root"),
1509        )
1510        .expect("workspace authority");
1511        let root = authority.output_root().expect("output authority");
1512        let output = root
1513            .open_beneath(Path::new("generated/python"))
1514            .expect("output authority");
1515
1516        let held = workspace.path().join("generated/python-held");
1517        fs::rename(workspace.path().join("generated/python"), &held)
1518            .expect("move retained output directory");
1519        symlink(outside.path(), workspace.path().join("generated/python"))
1520            .expect("redirect configured output path");
1521
1522        output
1523            .write_atomic("_models.py".as_ref(), b"retained authority")
1524            .expect("publication remains handle-relative");
1525        assert_eq!(
1526            fs::read(held.join("_models.py")).expect("retained output reads"),
1527            b"retained authority"
1528        );
1529        assert!(
1530            !outside.path().join("_models.py").exists(),
1531            "component replacement redirected output outside the workspace"
1532        );
1533    }
1534
1535    #[test]
1536    fn retained_output_root_survives_root_entry_swap_without_redirecting() {
1537        let workspace = tempfile::tempdir().expect("workspace directory");
1538        let outside = tempfile::tempdir().expect("outside directory");
1539        fs::create_dir_all(workspace.path().join("generated/python")).expect("output directory");
1540        let authority = WorkspaceDirectoryAuthority::open(
1541            WorkspaceRoot::new(fs::canonicalize(workspace.path()).expect("canonical workspace"))
1542                .expect("workspace root"),
1543        )
1544        .expect("workspace authority");
1545        let root = authority.output_root().expect("output authority");
1546        let held = workspace
1547            .path()
1548            .parent()
1549            .expect("temporary parent")
1550            .join(format!(
1551                "{}-retained",
1552                workspace
1553                    .path()
1554                    .file_name()
1555                    .expect("temporary name")
1556                    .to_string_lossy()
1557            ));
1558        fs::rename(workspace.path(), &held).expect("workspace root moves after validation");
1559        symlink(outside.path(), workspace.path()).expect("workspace name redirects outside");
1560
1561        let output = root
1562            .open_beneath(Path::new("generated/python"))
1563            .expect("output opens through retained root");
1564        output
1565            .write_atomic("_models.py".as_ref(), b"retained root authority")
1566            .expect("publication remains rooted in the retained handle");
1567        assert_eq!(
1568            fs::read(held.join("generated/python/_models.py")).expect("retained output reads"),
1569            b"retained root authority"
1570        );
1571        assert!(
1572            !outside.path().join("generated/python/_models.py").exists(),
1573            "root replacement redirected output outside the workspace"
1574        );
1575
1576        fs::remove_file(workspace.path()).expect("replacement symlink removes");
1577        fs::rename(&held, workspace.path()).expect("workspace restores for cleanup");
1578    }
1579}
1580
1581fn bind_approvals(
1582    graph: &type_bridge_schema_migration::MigrationHistoryGraph,
1583    approvals: &[String],
1584) -> Result<Vec<type_bridge_schema_migration::MigrationApplyApproval>, String> {
1585    if approvals.is_empty() {
1586        return Ok(Vec::new());
1587    }
1588    approvals
1589        .iter()
1590        .map(|compound| {
1591            let (app_label, name) = compound
1592                .split_once('/')
1593                .ok_or_else(|| format!("approval {compound:?} must be app-label/name"))?;
1594            let id = type_bridge_contract::migration::MigrationId::from_components(
1595                type_bridge_contract::migration::MigrationAppLabel::new(app_label.to_owned())
1596                    .map_err(display)?,
1597                type_bridge_contract::migration::MigrationName::new(name.to_owned())
1598                    .map_err(display)?,
1599            );
1600            let manifest = graph.manifest(&id).ok_or_else(|| {
1601                format!("approval target {compound:?} is not in the committed history")
1602            })?;
1603            type_bridge_schema_migration::MigrationApplyApproval::for_manifest(manifest)
1604                .map_err(display)
1605        })
1606        .collect()
1607}
1608
1609#[cfg(test)]
1610mod transport_option_tests {
1611    use super::*;
1612
1613    fn environment(policy: WorkspaceTransportPolicy) -> WorkspaceEnvironment {
1614        WorkspaceEnvironment::new(
1615            "typedb.example:1729",
1616            "example",
1617            SecretReference::environment("TYPEBRIDGE_TEST_USERNAME").expect("username reference"),
1618            SecretReference::environment("TYPEBRIDGE_TEST_PASSWORD").expect("password reference"),
1619        )
1620        .expect("environment")
1621        .with_transport_policy(policy)
1622    }
1623
1624    fn custom_root_workspace(root_bytes: &[u8]) -> (tempfile::TempDir, TypeBridgeWorkspace) {
1625        let directory = tempfile::tempdir().expect("workspace directory");
1626        fs::create_dir_all(directory.path().join("schema/fragments")).expect("schema directory");
1627        fs::create_dir_all(directory.path().join("migrations/v2")).expect("migration directory");
1628        fs::create_dir_all(directory.path().join("certs")).expect("certificate directory");
1629        fs::write(
1630            directory.path().join("schema/schema.yaml"),
1631            "format: typebridge.schema-set/v1\nsources: [fragments/*.yaml]\n",
1632        )
1633        .expect("schema set writes");
1634        fs::write(
1635            directory.path().join("schema/fragments/model.yaml"),
1636            "format: typebridge.schema/v2\nentities: {person: {}}\n",
1637        )
1638        .expect("schema writes");
1639        fs::write(directory.path().join("certs/root.pem"), root_bytes).expect("certificate writes");
1640        let manifest = directory.path().join("typebridge.yaml");
1641        fs::write(
1642            &manifest,
1643            "format: typebridge.workspace/v1\n\
1644             schema:\n  root: schema/schema.yaml\n  ownership: exclusive\n  managed-scope: tls-test\n\
1645             compatibility:\n  semantic-profile: typedb-3.12.1/v1\n\
1646             migrations:\n  directory: migrations/v2\n  app-label: tlstest\n\
1647             environments:\n  dev:\n    database: tls_test\n    uri: never-contact.invalid:1729\n    \
1648             tls: 'true'\n    tls-root-ca: certs/root.pem\n    credential:\n      username: \
1649             env:TYPEBRIDGE_TEST_USERNAME\n      password: env:TYPEBRIDGE_TEST_PASSWORD\n",
1650        )
1651        .expect("manifest writes");
1652        let workspace = load_workspace(&manifest).expect("custom-root workspace loads");
1653        (directory, workspace)
1654    }
1655
1656    #[test]
1657    fn workspace_transport_policy_maps_without_changing_plaintext_defaults() {
1658        let defaults = type_bridge_orm::SecureConnectOptions::default();
1659        let disabled = secure_connect_options(&environment(WorkspaceTransportPolicy::Disabled));
1660        assert_eq!(disabled.tls_mode, type_bridge_orm::TlsMode::Disabled);
1661        assert_eq!(disabled.http_port, defaults.http_port);
1662        assert_eq!(disabled.server_version, defaults.server_version);
1663
1664        let native = secure_connect_options(
1665            &environment(WorkspaceTransportPolicy::NativeRoots).with_http_port(9443),
1666        );
1667        assert_eq!(native.tls_mode, type_bridge_orm::TlsMode::NativeRoots);
1668        assert_eq!(native.http_port, 9443);
1669        assert_eq!(native.server_version, defaults.server_version);
1670    }
1671
1672    #[test]
1673    fn custom_root_mapping_preserves_the_validated_canonical_path() {
1674        let directory = tempfile::tempdir().expect("workspace directory");
1675        let canonical = fs::canonicalize(directory.path()).expect("canonical workspace");
1676        fs::create_dir_all(canonical.join("certs")).expect("certificate directory");
1677        fs::write(
1678            canonical.join("certs/root.pem"),
1679            b"not parsed at workspace boundary\n",
1680        )
1681        .expect("certificate writes");
1682        let root = WorkspaceRoot::new(canonical.clone()).expect("workspace root");
1683        let root_ca = type_bridge_workspace::WorkspaceRootCa::new(
1684            &root,
1685            "certs/root.pem",
1686            &SystemSchemaSourceService,
1687        )
1688        .expect("confined root CA");
1689
1690        let options = secure_connect_options(
1691            &environment(WorkspaceTransportPolicy::CustomRootCa(root_ca)).with_http_port(8443),
1692        );
1693        assert_eq!(
1694            options.tls_mode,
1695            type_bridge_orm::TlsMode::CustomRootCa(canonical.join("certs/root.pem"))
1696        );
1697        assert_eq!(options.http_port, 8443);
1698    }
1699
1700    #[test]
1701    fn malformed_custom_root_fails_transport_preflight_before_credentials_are_needed() {
1702        let (_directory, workspace) = custom_root_workspace(b"definitely not a certificate\n");
1703
1704        let error = preflight_secure_connect_options(&workspace, "dev")
1705            .expect_err("PEM parsing must happen before credential resolution");
1706        assert!(error.contains("tls_custom_root_ca_invalid_pem"), "{error}");
1707        assert!(!error.contains("TYPEBRIDGE_TEST_USERNAME"), "{error}");
1708        assert!(!error.contains("TYPEBRIDGE_TEST_PASSWORD"), "{error}");
1709    }
1710
1711    #[cfg(unix)]
1712    #[test]
1713    fn workspace_root_swap_to_outside_symlink_is_rejected_at_transport_preflight() {
1714        use std::os::unix::fs::symlink;
1715
1716        let (directory, workspace) = custom_root_workspace(b"initial regular root\n");
1717        let outside = tempfile::tempdir().expect("outside directory");
1718        let configured = directory.path().join("certs/root.pem");
1719
1720        let outside_root = outside.path().join("malicious.pem");
1721        fs::write(
1722            &outside_root,
1723            include_bytes!("../../core/tests/fixtures/valid-root.pem"),
1724        )
1725        .expect("write outside replacement root");
1726        fs::remove_file(&configured).expect("remove validated confined root");
1727        symlink(&outside_root, &configured).expect("install outside symlink after validation");
1728
1729        let error = preflight_secure_connect_options(&workspace, "dev")
1730            .expect_err("retained workspace paths must never follow a replacement symlink");
1731        assert!(error.contains("tls_custom_root_ca_unreadable"), "{error}");
1732        assert!(!error.contains("tls_custom_root_ca_invalid_pem"), "{error}");
1733    }
1734
1735    #[cfg(unix)]
1736    #[test]
1737    fn real_directory_root_replacement_cannot_substitute_custom_trust() {
1738        let (directory, workspace) =
1739            custom_root_workspace(include_bytes!("../../core/tests/fixtures/valid-root.pem"));
1740        let configured_root = directory.path().to_path_buf();
1741        let held_root = configured_root.with_extension("retained-custom-root-ca");
1742        fs::rename(&configured_root, &held_root).expect("move retained workspace root");
1743        fs::create_dir_all(configured_root.join("certs")).expect("replacement root creates");
1744        fs::write(
1745            configured_root.join("certs/root.pem"),
1746            b"attacker-controlled replacement is not a certificate\n",
1747        )
1748        .expect("replacement root writes");
1749
1750        let preflight = preflight_secure_connect_options(&workspace, "dev");
1751
1752        fs::remove_dir_all(&configured_root).expect("replacement root removes");
1753        fs::rename(&held_root, &configured_root).expect("retained root restores");
1754        preflight.expect("transport must use the CA under the retained original root");
1755    }
1756}
1757
1758#[cfg(test)]
1759mod adoption_file_tests {
1760    use super::*;
1761    use sha2::{Digest as _, Sha256};
1762
1763    const LEGACY_SCHEMA: &str = "define\nentity person;\n";
1764
1765    fn adoption_workspace() -> (tempfile::TempDir, TypeBridgeWorkspace) {
1766        let directory = tempfile::tempdir().expect("workspace directory");
1767        fs::create_dir_all(directory.path().join("schema/fragments")).expect("schema directory");
1768        fs::write(
1769            directory.path().join("schema/schema.yaml"),
1770            "format: typebridge.schema-set/v1\nsources: [fragments/*.yaml]\n",
1771        )
1772        .expect("schema set writes");
1773        fs::write(
1774            directory.path().join("schema/fragments/model.yaml"),
1775            "format: typebridge.schema/v2\nentities: {person: {}}\n",
1776        )
1777        .expect("schema writes");
1778        let manifest = directory.path().join("typebridge.yaml");
1779        fs::write(
1780            &manifest,
1781            "format: typebridge.workspace/v1\n\
1782             schema:\n  root: schema/schema.yaml\n  ownership: exclusive\n  managed-scope: adoption-test\n\
1783             compatibility:\n  semantic-profile: typedb-3.12.1/v1\n\
1784             migrations:\n  directory: migrations/v2\n  app-label: smoke\n\
1785             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",
1786        )
1787        .expect("manifest writes");
1788        let workspace = load_workspace(&manifest).expect("workspace loads");
1789        (directory, workspace)
1790    }
1791
1792    fn write_legacy_fixture(root: &Path) -> PathBuf {
1793        let directory = root.join("migrations/legacy");
1794        fs::create_dir_all(&directory).expect("legacy directory");
1795        let name = "0001_initial";
1796        let python_source = "class Migration:\n    operations = []\n";
1797        let checksum = type_bridge_migration::migration_file_checksum(python_source);
1798        let source_sha256 = format!("{:x}", Sha256::digest(python_source.as_bytes()));
1799        let schema_hash = format!("{:x}", Sha256::digest(LEGACY_SCHEMA.as_bytes()));
1800        fs::write(directory.join(format!("{name}.py")), python_source)
1801            .expect("legacy source writes");
1802        let adoption = type_bridge_migration::LegacyAdoptionMetadata::new(
1803            "legacy",
1804            name,
1805            Vec::new(),
1806            checksum,
1807            source_sha256,
1808            type_bridge_migration::LegacySchemaEffect::Snapshot,
1809            type_bridge_migration::MigrationDependencySpec {
1810                app_label: "legacy".to_owned(),
1811                migration_name: name.to_owned(),
1812            },
1813            schema_hash.clone(),
1814        )
1815        .expect("legacy adoption metadata");
1816        fs::write(
1817            directory.join(format!("{name}.adoption.json")),
1818            serde_json::to_vec_pretty(&adoption).expect("metadata encodes"),
1819        )
1820        .expect("metadata writes");
1821        let snapshot = directory.join("snapshots/v0001");
1822        fs::create_dir_all(&snapshot).expect("snapshot directory");
1823        fs::write(snapshot.join("schema.tql"), LEGACY_SCHEMA).expect("snapshot schema writes");
1824        fs::write(
1825            snapshot.join("snapshot.json"),
1826            serde_json::to_vec_pretty(&serde_json::json!({
1827                "version": "v0001",
1828                "source_migration": name,
1829                "schema_hash": schema_hash,
1830                "file_hashes": {"schema.tql": schema_hash},
1831                "type_bridge_version": "1.5.11",
1832                "type_bridge_core_version": "1.5.11"
1833            }))
1834            .expect("snapshot manifest encodes"),
1835        )
1836        .expect("snapshot manifest writes");
1837        directory
1838    }
1839
1840    #[test]
1841    fn authority_publication_is_atomic_no_replace_and_resumable() {
1842        let directory = tempfile::tempdir().expect("directory");
1843        let authority =
1844            type_bridge_schema_migration::MigrationDirectory::open_ambient(directory.path())
1845                .expect("directory authority");
1846        let name = "adopted-genesis.typeql";
1847        let path = directory.path().join("adopted-genesis.typeql");
1848        assert!(
1849            publish_authority(&authority, name, b"define\nentity person;\n")
1850                .expect("first publish")
1851        );
1852        assert!(
1853            !publish_authority(&authority, name, b"define\nentity person;\n")
1854                .expect("identical recovery")
1855        );
1856        assert!(publish_authority(&authority, name, b"define\nentity company;\n").is_err());
1857        assert_eq!(
1858            fs::read(&path).expect("authority reads"),
1859            b"define\nentity person;\n"
1860        );
1861        assert!(
1862            fs::read_dir(directory.path())
1863                .expect("directory reads")
1864                .all(|entry| !entry
1865                    .expect("entry")
1866                    .file_name()
1867                    .to_string_lossy()
1868                    .ends_with(".tmp"))
1869        );
1870    }
1871
1872    #[cfg(unix)]
1873    #[test]
1874    fn authority_publication_rejects_final_symlink() {
1875        use std::os::unix::fs::symlink;
1876
1877        let directory = tempfile::tempdir().expect("directory");
1878        let outside = directory.path().join("outside");
1879        fs::write(&outside, b"untouched").expect("outside writes");
1880        let path = directory.path().join("adopted-genesis.typeql");
1881        symlink(&outside, &path).expect("symlink");
1882        let authority =
1883            type_bridge_schema_migration::MigrationDirectory::open_ambient(directory.path())
1884                .expect("directory authority");
1885
1886        assert!(publish_authority(&authority, "adopted-genesis.typeql", b"replacement").is_err());
1887        assert_eq!(fs::read(&outside).expect("outside reads"), b"untouched");
1888    }
1889
1890    #[test]
1891    fn invalid_adoption_name_creates_no_canonical_directory() {
1892        let (directory, workspace) = adoption_workspace();
1893        let missing_legacy = directory.path().join("missing-legacy");
1894        let error = run_connected(
1895            &workspace,
1896            "dev",
1897            ConnectedAction::Adopt {
1898                legacy_directory: missing_legacy,
1899                name: String::new(),
1900            },
1901        )
1902        .expect_err("invalid name fails before history or network access");
1903        assert!(error.contains("migration"), "{error}");
1904        assert!(
1905            !directory.path().join("migrations/v2").exists(),
1906            "bad-name validation must not create canonical filesystem state"
1907        );
1908    }
1909
1910    #[test]
1911    fn invalid_apply_approvals_fail_before_credentials_or_network() {
1912        let (directory, workspace) = adoption_workspace();
1913        fs::create_dir_all(directory.path().join("migrations/v2")).expect("canonical directory");
1914
1915        for (approval, expected) in [
1916            ("not-a-compound-id", "must be app-label/name"),
1917            ("smoke/0001_missing", "is not in the committed history"),
1918        ] {
1919            let error = run_connected(
1920                &workspace,
1921                "dev",
1922                ConnectedAction::Apply {
1923                    approvals: vec![approval.to_owned()],
1924                },
1925            )
1926            .expect_err("invalid approval is rejected by local authority");
1927            assert!(error.contains(expected), "{approval}: {error}");
1928            assert!(
1929                !error.contains("credential")
1930                    && !error.contains("connect")
1931                    && !error.contains("database"),
1932                "approval validation ran after external setup: {error}"
1933            );
1934        }
1935    }
1936
1937    #[test]
1938    fn adoption_retry_completes_either_exact_orphan_direction() {
1939        for orphan in ["genesis", "bridge"] {
1940            let (directory, workspace) = adoption_workspace();
1941            let legacy = write_legacy_fixture(directory.path());
1942            let prepared = prepare_legacy_adoption(&workspace, &legacy, "0000_legacy_frontier")
1943                .expect("adoption prepares");
1944            let migration_directory = workspace
1945                .ensure_migration_directory()
1946                .expect("canonical directory");
1947            match orphan {
1948                "genesis" => {
1949                    publish_authority(
1950                        migration_directory.directory(),
1951                        type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME,
1952                        prepared.reconstructed.schema_typeql().as_bytes(),
1953                    )
1954                    .expect("genesis orphan publishes");
1955                }
1956                "bridge" => {
1957                    publish_authority(
1958                        migration_directory.directory(),
1959                        &prepared.bridge_name,
1960                        &prepared.bridge_bytes,
1961                    )
1962                    .expect("bridge orphan publishes");
1963                }
1964                _ => unreachable!(),
1965            }
1966
1967            publish_prepared_adoption(&workspace, &migration_directory, &prepared)
1968                .expect("adoption retry completes the exact orphan");
1969            workspace
1970                .discover_migrations_in(&migration_directory)
1971                .expect("completed adoption pair discovers");
1972            assert!(
1973                migration_directory
1974                    .display_path()
1975                    .join(type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME)
1976                    .is_file()
1977            );
1978            assert!(
1979                migration_directory
1980                    .display_path()
1981                    .join(&prepared.bridge_name)
1982                    .is_file()
1983            );
1984        }
1985    }
1986
1987    #[test]
1988    fn legacy_history_race_after_bridge_rolls_back_new_publication() {
1989        let (directory, workspace) = adoption_workspace();
1990        let legacy = write_legacy_fixture(directory.path());
1991        let prepared = prepare_legacy_adoption(&workspace, &legacy, "0000_legacy_frontier")
1992            .expect("adoption prepares");
1993        let migration_directory = workspace
1994            .ensure_migration_directory()
1995            .expect("canonical directory");
1996        let legacy_source = legacy.join("0001_initial.py");
1997
1998        let error = publish_prepared_adoption_with_after_bridge(
1999            &workspace,
2000            &migration_directory,
2001            &prepared,
2002            || {
2003                fs::write(
2004                    &legacy_source,
2005                    "class Migration:\n    operations = ['changed']\n",
2006                )
2007                .expect("race mutation writes");
2008            },
2009        )
2010        .expect_err("legacy authority race aborts pair publication");
2011        assert!(error.contains("changed"), "{error}");
2012        assert!(
2013            !migration_directory
2014                .display_path()
2015                .join(&prepared.bridge_name)
2016                .exists(),
2017            "new bridge is rolled back"
2018        );
2019        assert!(
2020            !migration_directory
2021                .display_path()
2022                .join(type_bridge_schema_compat::ADOPTED_GENESIS_FILE_NAME)
2023                .exists(),
2024            "genesis is never published after the race"
2025        );
2026    }
2027}