diff --git a/docs/briefs/r3-omni-openapi-backend-bridge-dissolution.md b/docs/briefs/r3-omni-openapi-backend-bridge-dissolution.md new file mode 100644 index 00000000000..e66e2446d0e --- /dev/null +++ b/docs/briefs/r3-omni-openapi-backend-bridge-dissolution.md @@ -0,0 +1,39 @@ +# R3 Omni OpenAPI Backend Bridge Dissolution + +Status: QUEUED + +Owner lane: T-PB-B, coordinated with T-Omni-Shape-B. + +## Scope + +Retire the R3 gate #25 hand-Rust demo bridge introduced for the omni OpenAPI +backend emission receipt: + +- `src/v3/compiler/src/omni_shape_b_openapi.rs` + `project_openapi_yaml`, `project_markdown_documentation`, and + `project_rust_backend_service` +- `src/v3/compiler/tests/integration/m1_5_omni_shape_b_openapi_test.rs` + direct `rustc` generated-backend roundtrip helper + +These are fixture-scoped demo projections, not compiler targets and not a +general backend framework. + +## Dissolution Trigger + +Retire this bridge when `.dag` TestClaim execution can materialize a generated +artifact, invoke `ExecuteCommand` against it, and reuse the produced binary or +artifact handle across multiple assertions. At that point: + +- OpenAPI and Markdown projection move to Shape B `.dag` artifact emitters. +- The runnable backend uses the normal Shape A Rust emission path or generated + artifact materialization path, not a compiler-local string projector. +- The route roundtrip moves from Rust-authored `Command::new("rustc")` to an + `ExecuteCommand`-based `.dag` TestClaim. + +## ROADMAP Receipt + +`ROADMAP.md:170` — Hand-Rust census split; the test-driven bridge retires +through T-PB-B by migrating the generated-backend roundtrip to a `.dag` +TestClaim declaration. The adjacent production projection is in this brief's +scope only because that test bridge is its sole consumer for the R3 gate #25 +receipt; it retires with the same T-PB-B TestClaim migration. diff --git a/src/v3/compiler/src/bootstrap_generated.rs b/src/v3/compiler/src/bootstrap_generated.rs index db4cb3764aa..48bed69169b 100644 --- a/src/v3/compiler/src/bootstrap_generated.rs +++ b/src/v3/compiler/src/bootstrap_generated.rs @@ -89137,7 +89137,7 @@ fn bootstrapped_fixture_dag_declarations() -> Vec { value_body: None, refinement: None, nominal_opacity: None, - span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8805, 8812), + span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8783, 8790), }); declarations.push(Declaration { id: DeclarationId(3038), @@ -89151,7 +89151,7 @@ fn bootstrapped_fixture_dag_declarations() -> Vec { value_body: None, refinement: None, nominal_opacity: None, - span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8805, 8812), + span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8783, 8790), }); declarations.push(Declaration { id: DeclarationId(3039), @@ -89176,7 +89176,7 @@ fn bootstrapped_fixture_dag_declarations() -> Vec { value_body: None, refinement: None, nominal_opacity: None, - span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8805, 8812), + span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8783, 8790), }); declarations.push(Declaration { id: DeclarationId(3040), @@ -89195,7 +89195,7 @@ fn bootstrapped_fixture_dag_declarations() -> Vec { value_body: None, refinement: None, nominal_opacity: None, - span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8873, 8878), + span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8851, 8856), }); declarations.push(Declaration { id: DeclarationId(3041), @@ -89209,7 +89209,7 @@ fn bootstrapped_fixture_dag_declarations() -> Vec { value_body: None, refinement: None, nominal_opacity: None, - span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8873, 8878), + span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8851, 8856), }); declarations.push(Declaration { id: DeclarationId(3042), @@ -89234,7 +89234,7 @@ fn bootstrapped_fixture_dag_declarations() -> Vec { value_body: None, refinement: None, nominal_opacity: None, - span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8873, 8878), + span: SourceSpan::new("src/v3/std/anthropic_schema.dag", 8851, 8856), }); declarations.push(Declaration { id: DeclarationId(3043), diff --git a/src/v3/compiler/src/bootstrap_generated_without_parse_surface.rs b/src/v3/compiler/src/bootstrap_generated_without_parse_surface.rs index 90dc0bc1a08..b5a64251ff6 100644 --- a/src/v3/compiler/src/bootstrap_generated_without_parse_surface.rs +++ b/src/v3/compiler/src/bootstrap_generated_without_parse_surface.rs @@ -84702,7 +84702,7 @@ fn bootstrapped_fixture_without_parse_surface_dag_declarations() -> Vec Vec Vec Vec Vec Vec Result Result { + let routes = extract_rest_routes(dag)?; + let mut out = String::from( + r#"use std::io::{Read, Write}; +use std::net::TcpListener; + +#[derive(Clone, Copy)] +struct Route { + method: &'static str, + path: &'static str, +} + +const ROUTES: &[Route] = &[ +"#, + ); + for route in &routes { + out.push_str(" Route { method: "); + out.push_str(&rust_string_literal(&route.method)); + out.push_str(", path: "); + out.push_str(&rust_string_literal(&route.path)); + out.push_str(" },\n"); + } + out.push_str( + r#"]; + +fn route_status(method: &str, path: &str) -> u16 { + if ROUTES + .iter() + .any(|route| route.method == method && path_matches(route.path, path)) + { + 200 + } else { + 404 + } +} + +fn path_matches(template: &str, path: &str) -> bool { + let template = strip_leading_slash(template); + let path = strip_leading_slash(path); + let template_parts: Vec<_> = template.split('/').collect(); + let path_parts: Vec<_> = path.split('/').collect(); + if template_parts.len() != path_parts.len() { + return false; + } + template_parts + .iter() + .zip(path_parts.iter()) + .all(|(template_part, path_part)| segment_matches(template_part, path_part)) +} + +fn strip_leading_slash(value: &str) -> &str { + value.strip_prefix('/').unwrap_or(value) +} + +fn segment_matches(template: &str, path: &str) -> bool { + let mut remainder = path; + let mut rest = template; + while let Some(start) = rest.find('{') { + let literal = &rest[..start]; + if !remainder.starts_with(literal) { + return false; + } + remainder = &remainder[literal.len()..]; + let after_open = &rest[start + 1..]; + let Some(end) = after_open.find('}') else { + return false; + }; + let after_param = &after_open[end + 1..]; + if let Some(next_literal_start) = after_param.find('{') { + let next_literal = &after_param[..next_literal_start]; + if next_literal.is_empty() { + return false; + } + let Some(boundary) = remainder.find(next_literal) else { + return false; + }; + let param_value = &remainder[..boundary]; + if !slash_free_non_empty(param_value) { + return false; + } + remainder = &remainder[boundary..]; + rest = after_param; + } else { + if after_param.is_empty() { + return slash_free_non_empty(remainder); + } + let Some(value) = remainder.strip_suffix(after_param) else { + return false; + }; + return slash_free_non_empty(value); + } + } + remainder == rest +} + +fn slash_free_non_empty(value: &str) -> bool { + !value.is_empty() && !value.contains('/') +} + +fn respond(request: &str) -> String { + let request_line = request.lines().next().unwrap_or(""); + let mut parts = request_line.split_whitespace(); + let method = parts.next().unwrap_or(""); + let path = parts.next().unwrap_or(""); + let status = route_status(method, path); + let reason = if status == 200 { "OK" } else { "Not Found" }; + format!("HTTP/1.1 {status} {reason}\r\ncontent-length: 0\r\n\r\n") +} + +fn serve(addr: &str) -> std::io::Result<()> { + let listener = TcpListener::bind(addr)?; + for stream in listener.incoming() { + let mut stream = stream?; + let mut request = [0_u8; 2048]; + let bytes = stream.read(&mut request)?; + let response = respond(&String::from_utf8_lossy(&request[..bytes])); + stream.write_all(response.as_bytes())?; + } + Ok(()) +} + +fn main() { + let args: Vec = std::env::args().collect(); + match args.as_slice() { + [_, flag, method, path] if flag == "--probe" => { + println!("{}", route_status(method, path)); + } + [_, flag, addr] if flag == "--serve" => { + serve(addr).expect("serve backend"); + } + _ => { + for route in ROUTES { + println!("{} {}", route.method, route.path); + } + } + } +} +"#, + ); + Ok(out) +} + fn append_path_parameter_yaml(out: &mut String, parameter: &str) { out.push_str(" - name: "); out.push_str(&yaml_double_quoted(parameter)); @@ -437,6 +592,25 @@ fn yaml_quoted(value: &str) -> String { format!("'{}'", value.replace('\'', "''")) } +fn rust_string_literal(value: &str) -> String { + let mut literal = String::from("\""); + for ch in value.chars() { + match ch { + '\\' => literal.push_str("\\\\"), + '"' => literal.push_str("\\\""), + '\n' => literal.push_str("\\n"), + '\r' => literal.push_str("\\r"), + '\t' => literal.push_str("\\t"), + other if other.is_control() => { + write!(&mut literal, "\\u{{{:X}}}", other as u32).expect("write to String"); + } + other => literal.push(other), + } + } + literal.push('"'); + literal +} + fn yaml_double_quoted(value: &str) -> String { let mut quoted = String::from("\""); for ch in value.chars() { diff --git a/src/v3/compiler/tests/integration/m1_5_omni_shape_b_openapi_test.rs b/src/v3/compiler/tests/integration/m1_5_omni_shape_b_openapi_test.rs index 8d0ca86a080..a219b64efcb 100644 --- a/src/v3/compiler/tests/integration/m1_5_omni_shape_b_openapi_test.rs +++ b/src/v3/compiler/tests/integration/m1_5_omni_shape_b_openapi_test.rs @@ -8,13 +8,19 @@ //! cross-target TestPredicate variant exists. use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::sync::atomic::{AtomicUsize, Ordering}; use v3_compiler::compile_to_dag; use v3_compiler::emit_rust::emit_rust; use v3_compiler::omni_shape_b_openapi::{ - extract_rest_routes, project_markdown_documentation, project_openapi_yaml, RestRoute, + extract_rest_routes, project_markdown_documentation, project_openapi_yaml, + project_rust_backend_service, RestRoute, }; +static BACKEND_ROUNDTRIP_ID: AtomicUsize = AtomicUsize::new(0); + const OMNI_SERVICE_FIXTURE: &str = r#" module t.openapi_demo @@ -171,6 +177,69 @@ fn expected_routes() -> BTreeSet { ]) } +struct TmpDir(PathBuf); + +impl TmpDir { + fn new() -> Self { + let id = BACKEND_ROUNDTRIP_ID.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "v3_omni_openapi_backend_{}_{}", + std::process::id(), + id + )); + std::fs::create_dir_all(&path).expect("create backend temp dir"); + TmpDir(path) + } + + fn path(&self) -> &Path { + &self.0 + } +} + +impl Drop for TmpDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +fn compile_backend_service(source: &str) -> (TmpDir, PathBuf) { + let tmp_dir = TmpDir::new(); + let src_path = tmp_dir.path().join("omni_backend.rs"); + let bin_path = tmp_dir.path().join("omni_backend"); + std::fs::write(&src_path, source).expect("write generated backend service"); + // P5 bridge bound: this direct rustc invocation retires through the queued + // brief `docs/briefs/r3-omni-openapi-backend-bridge-dissolution.md`. + let compile = Command::new("rustc") + .env_remove("RUSTC_BOOTSTRAP") + .arg("--edition=2021") + .arg(&src_path) + .arg("-o") + .arg(&bin_path) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .expect("invoke rustc for generated backend service"); + assert!( + compile.success(), + "rustc failed on generated backend service:\n{source}" + ); + (tmp_dir, bin_path) +} + +fn backend_probe(bin_path: &Path, method: &str, path: &str) -> String { + let output = Command::new(bin_path) + .arg("--probe") + .arg(method) + .arg(path) + .output() + .expect("run generated backend probe"); + assert!( + output.status.success(), + "generated backend probe exits zero" + ); + String::from_utf8_lossy(&output.stdout).trim().to_string() +} + fn openapi_yaml_routes(yaml: &str) -> BTreeSet { let mut routes: BTreeMap<(String, String), BTreeSet> = BTreeMap::new(); let mut current_path: Option = None; @@ -347,6 +416,8 @@ fn omni_layers_share_one_node_tree() { let _canonical_routes = extract_rest_routes(&dag).expect("canonical route projection extracts"); let _openapi = project_openapi_yaml(&dag).expect("Shape B OpenAPI projects from shared DAG"); + let _backend = + project_rust_backend_service(&dag).expect("backend service projects from shared DAG"); let _markdown = project_markdown_documentation(&dag).expect("Shape B Markdown projects from shared DAG"); let _rust = emit_rust(&dag).expect("Shape A Rust emit consumes shared DAG"); @@ -354,7 +425,7 @@ fn omni_layers_share_one_node_tree() { assert_eq!( compile_count, 1, "Shape A + Shape B layers must consume the same compile_to_dag result \ - (routes + OpenAPI + Markdown + emit_rust); recompiling per layer would \ + (routes + OpenAPI + backend + Markdown + emit_rust); recompiling per layer would \ break the structural-fold receipt." ); } @@ -395,6 +466,43 @@ fn openapi_routes_match_canonical_dag_routes_interim() { assert_eq!(openapi_routes, canonical_routes); } +#[test] +fn omni_openapi_backend_emission_demo_generates_runnable_matching_backend() { + let dag = compile_omni_service_fixture(); + + let canonical_routes = extract_rest_routes(&dag).expect("canonical route projection extracts"); + let openapi_routes = + openapi_yaml_routes(&project_openapi_yaml(&dag).expect("OpenAPI YAML projects")); + let backend_source = + project_rust_backend_service(&dag).expect("backend service projects from shared DAG"); + let (_tmp_dir, backend_bin) = compile_backend_service(&backend_source); + + assert_eq!(openapi_routes, canonical_routes); + for route in &canonical_routes { + let concrete_path = route + .path + .replace("{id}", "42") + .replace("{org}", "gunb-ai") + .replace("{repo}", "gunbc") + .replace("{secret_name}", "api-key"); + assert_eq!( + backend_probe(&backend_bin, &route.method, &concrete_path), + "200", + "generated backend accepts route {} {}", + route.method, + concrete_path + ); + } + assert_eq!(backend_probe(&backend_bin, "GET", "/missing"), "404"); + assert_eq!(backend_probe(&backend_bin, "DELETE", "/users"), "404"); + assert_eq!(backend_probe(&backend_bin, "GET", "/users/"), "404"); + assert_eq!(backend_probe(&backend_bin, "GET", "/users/42/extra"), "404"); + assert_eq!( + backend_probe(&backend_bin, "POST", "/secrets/api/key:addVersion"), + "404" + ); +} + #[test] fn shape_b_markdown_documentation_drift_locks_to_canonical_dag_routes() { let dag = compile_omni_service_fixture();