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