diff --git a/crates/codestory-bench/Cargo.toml b/crates/codestory-bench/Cargo.toml index 713656c2d..47e0110a1 100644 --- a/crates/codestory-bench/Cargo.toml +++ b/crates/codestory-bench/Cargo.toml @@ -34,6 +34,10 @@ path = "src/bin/codestory_witness_seam.rs" name = "codestory-etr1" path = "src/bin/codestory_etr1.rs" +[[bin]] +name = "codestory-str1" +path = "src/bin/codestory_str1.rs" + [dev-dependencies] codestory-retrieval = { workspace = true, features = ["benchmark-support"] } criterion = { workspace = true } diff --git a/crates/codestory-bench/src/bin/codestory_etr1/run.rs b/crates/codestory-bench/src/bin/codestory_etr1/run.rs index bc0175e3f..35abc687c 100644 --- a/crates/codestory-bench/src/bin/codestory_etr1/run.rs +++ b/crates/codestory-bench/src/bin/codestory_etr1/run.rs @@ -12,6 +12,10 @@ use std::fs; use std::path::{Path, PathBuf}; use std::time::Instant; +// Only the separate measurement binary calls this module; ETR stays frozen. +#[path = "str1.rs"] +pub mod str1; + const RUN_CONTRACT: &str = "codestory.etr1-run/v1"; const ROW_CONTRACT: &str = "codestory.etr1-wording/v1"; const VECTOR_CONTRACT: &str = "codestory.embedding-diagnostic-output/v1"; diff --git a/crates/codestory-bench/src/bin/codestory_etr1/str1.rs b/crates/codestory-bench/src/bin/codestory_etr1/str1.rs new file mode 100644 index 000000000..fcd4e4a7c --- /dev/null +++ b/crates/codestory-bench/src/bin/codestory_etr1/str1.rs @@ -0,0 +1,740 @@ +use super::*; +use codestory_contracts::graph::{NodeKind, ResolutionCertainty}; +use codestory_store::CoreReadSession; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct GraphNode { + pub id: i64, + pub fragment_ids: Vec, +} +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Witness { + pub path: String, + pub start_line: u32, + pub end_line: u32, + pub content_digest: String, + pub source: String, +} +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Relation { + pub id: String, + pub source: i64, + pub target: i64, + pub certainty: String, + pub occurrence: Witness, + pub occurrence_fragment_ids: Vec, + pub raw: Value, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Graph { + pub repository_id: String, + pub preparation: FileBinding, + pub core: FileBinding, + pub core_pointer: Value, + pub source_bindings: Vec, + pub nodes: Vec, + pub relations: Vec, + pub gaps: Vec, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Step { + pub seed_fragment_id: String, + pub anchors: Vec, + pub relations: Vec, + pub eligible: Vec, + pub excluded_before: Vec, + pub retained_successors: Vec, + pub boundary_gaps: Vec, +} +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Job { + operation: String, + preparation: FileBinding, + method: FileBinding, + #[serde(default)] + graph_inputs: Option, + #[serde(default)] + graph_preparations: BTreeMap, + #[serde(default)] + graphs: Option, + #[serde(default)] + vectors: Option, + #[serde(default)] + control_run: Option, + #[serde(default)] + state_root: Option, + cancel_file: PathBuf, + output: PathBuf, +} + +#[derive(Debug, Deserialize)] +struct GraphInput { + repository_id: String, + preparation: FileBinding, + core: FileBinding, + pointer: FileBinding, +} +#[derive(Debug, Deserialize)] +struct GraphInputs { + repositories: Vec, +} + +fn fixed_inputs(job: &Job, preparation: &Etr1PreparationV1) -> Result> { + ensure!( + job.method.sha256 == "0902f8f8f6771fce5c6addf09bbdef061fd3706be0da050c32baead80fb171fc", + "unregistered_structural_method" + ); + read_bound_json::(&job.method)?; + if preparation.authority == "synthetic_canary_only" { + ensure!( + preparation.repositories.len() == 1 + && preparation.fragments.len() == 32 + && preparation.wordings.len() == 3, + "invalid_canary_shape" + ); + return Ok(Vec::new()); + } + ensure!( + preparation.authority == "visible_development_frontier_only", + "invalid_structural_authority" + ); + ensure!( + job.preparation.sha256 + == "30b84d4d848f96bd4fe799f2e0f28b9114971da0e47bf98ebe54fe36242199fd", + "unregistered_preparation" + ); + let binding = job + .graph_inputs + .as_ref() + .context("graph_input_freeze_missing")?; + ensure!( + binding.sha256 == "668c990ee29b25a4bab0cb03e048d70eebd8ffe3dd62317d69b2e58b912a2c9f", + "unregistered_graph_inputs" + ); + let inputs: GraphInputs = read_bound_json(binding)?; + let expected = preparation + .repositories + .iter() + .map(|r| r.repository_id.as_str()) + .collect::>(); + ensure!( + inputs.repositories.len() == expected.len() + && inputs + .repositories + .iter() + .map(|r| r.repository_id.as_str()) + .collect::>() + == expected, + "graph_repository_set_changed" + ); + for input in &inputs.repositories { + for file in [&input.preparation, &input.core, &input.pointer] { + ensure!( + bind_file(&file.path, Some(&file.sha256))? == *file, + "graph_input_changed" + ); + } + let wal = PathBuf::from(format!("{}-wal", input.core.path.display())); + ensure!( + !wal.exists() || fs::metadata(wal)?.len() == 0, + "unbound_graph_wal" + ); + if job.operation == "export_graphs" { + ensure!( + job.graph_preparations.get(&input.repository_id) == Some(&input.preparation), + "unregistered_graph_preparation" + ); + } + } + if let Some(vectors) = &job.vectors { + ensure!( + vectors.sha256 == "7f604b30b823066bd5b0ed71106d10577c28495abd270444bc8ad5b7a63cb70a", + "unregistered_vectors" + ); + } + if let Some(control) = &job.control_run { + ensure!( + control.sha256 == "c14da697d03707c0096f5f2fd7a97bff2ab5b4a6f9326c4a9a03da2066d545f2", + "unregistered_control" + ); + } + Ok(inputs.repositories) +} + +fn authenticate_graph_input(graph: &Graph, inputs: &[GraphInput]) -> Result<()> { + if inputs.is_empty() { + return Ok(()); + } + let input = inputs + .iter() + .find(|i| i.repository_id == graph.repository_id) + .context("unregistered_graph")?; + ensure!( + graph.core == input.core + && graph.preparation == input.preparation + && graph.core_pointer == read_bound_json::(&input.pointer)?, + "graph_authority_changed" + ); + Ok(()) +} + +fn overlaps(fragment: &FrozenFragmentV1, path: &str, start: u32, end: u32) -> bool { + fragment.path == path && fragment.line_range.start <= end && start <= fragment.line_range.end +} + +fn graph_for( + repository: &PreparedRepositoryV1, + fragments: &[FrozenFragmentV1], + binding: &FileBinding, +) -> Result { + let prepared: Value = read_bound_json(binding)?; + ensure!( + prepared["project_root"] == repository.local_root.to_string_lossy().as_ref(), + "graph_root_mismatch" + ); + let storage = PathBuf::from( + prepared["storage_path"] + .as_str() + .context("graph_storage_missing")?, + ); + let pin = CoreReadSession::pin(&storage)?; + ensure!( + serde_json::to_value(pin.pointer())? == prepared["core_pointer"], + "graph_pointer_drift" + ); + ensure!( + prepared["publication"] == repository.publication, + "graph_publication_mismatch" + ); + ensure!( + super::super::prepare::git_head(&repository.local_root)? == repository.commit, + "graph_repository_drift" + ); + let store = pin.storage(); + let core = bind_file(pin.generation_path(), None)?; + let mut files = BTreeMap::new(); + let mut sources = BTreeMap::new(); + let mut source_bindings = Vec::new(); + for file in store.get_files()? { + let source_path = if file.path.is_absolute() { + file.path.clone() + } else { + repository.local_root.join(&file.path) + }; + let canonical = fs::canonicalize(&source_path)?; + ensure!( + canonical.starts_with(&repository.local_root), + "graph_source_escape" + ); + let relative = canonical + .strip_prefix(&repository.local_root)? + .to_string_lossy() + .to_string(); + let bytes = fs::read(&canonical)?; + let digest = sha256(&bytes); + ensure!( + store.get_file_content_hash(file.id)?.as_deref() == Some(digest.as_str()), + "graph_source_hash_drift:{}", + relative + ); + source_bindings.push(bind_file(&canonical, Some(&digest))?); + // Non-UTF-8/missing-precision source cannot supply a positive witness. + if let Ok(source) = String::from_utf8(bytes) { + sources.insert(file.id, (relative.clone(), digest, source)); + } + files.insert(file.id, relative); + } + let map_range = |path: &str, start: u32, end: u32| { + fragments + .iter() + .filter(|f| f.project_id == repository.project_id && overlaps(f, path, start, end)) + .map(|f| f.fragment_id.clone()) + .collect::>() + }; + let mut nodes = Vec::new(); + for node in store.get_nodes()? { + let ids = if node.kind != NodeKind::FILE { + match (node.file_node_id, node.start_line, node.end_line) { + (Some(file), Some(start), Some(end)) if start > 0 && end >= start => files + .get(&file.0) + .map(|path| map_range(path, start, end)) + .unwrap_or_default(), + _ => Vec::new(), + } + } else { + Vec::new() + }; + nodes.push(GraphNode { + id: node.id.0, + fragment_ids: ids, + }); + } + nodes.sort_by_key(|n| n.id); + let node_ids = nodes.iter().map(|n| n.id).collect::>(); + let mut relations = Vec::new(); + let mut gaps = Vec::new(); + for edge in store.get_edges()? { + if edge.certainty != Some(ResolutionCertainty::Certain) { + continue; + } + let source = edge.effective_source().0; + let target = edge.effective_target().0; + if !node_ids.contains(&source) || !node_ids.contains(&target) { + gaps.push(json!({"edge_id": edge.id.0,"kind":"missing_effective_endpoint"})); + continue; + } + let mut locations = match (edge.file_node_id, edge.line) { + (Some(file), Some(line)) if line > 0 => vec![(file.0, line, line)], + _ => Vec::new(), + }; + if locations.is_empty() { + for occurrence in store.get_occurrences_for_element(edge.id.0)? { + if matches!( + occurrence.kind, + codestory_contracts::graph::OccurrenceKind::REFERENCE + | codestory_contracts::graph::OccurrenceKind::MACRO_REFERENCE + ) { + locations.push(( + occurrence.location.file_node_id.0, + occurrence.location.start_line, + occurrence.location.end_line, + )); + } + } + } + locations.sort(); + locations.dedup(); + let witnesses = locations + .into_iter() + .filter_map(|(file, start, end)| { + let (path, digest, text) = sources.get(&file)?; + let lines = text.split_inclusive('\n').collect::>(); + if start == 0 || end < start || end as usize > lines.len() { + return None; + } + let source = lines[start as usize - 1..end as usize].concat(); + if source.trim().is_empty() { + return None; + } + Some(Witness { + path: path.clone(), + start_line: start, + end_line: end, + content_digest: digest.clone(), + source, + }) + }) + .collect::>(); + if witnesses.is_empty() { + gaps.push(json!({"edge_id":edge.id.0,"kind":"missing_positive_occurrence"})); + continue; + } + for occurrence in witnesses { + let occurrence_fragment_ids = + map_range(&occurrence.path, occurrence.start_line, occurrence.end_line); + relations.push(Relation { + id: format!( + "{}:{}:{}:{}", + edge.id.0, occurrence.path, occurrence.start_line, occurrence.end_line + ), + source, + target, + certainty: "Certain".into(), + occurrence, + occurrence_fragment_ids, + raw: serde_json::to_value(&edge)?, + }); + } + } + relations.sort_by(|a, b| a.id.cmp(&b.id)); + ensure!( + bind_file(pin.generation_path(), Some(&core.sha256))? == core, + "graph_core_changed" + ); + Ok(Graph { + repository_id: repository.repository_id.clone(), + preparation: binding.clone(), + core, + core_pointer: prepared["core_pointer"].clone(), + source_bindings, + nodes, + relations, + gaps, + }) +} + +pub fn frontier( + graph: &Graph, + seeds: &[String], + scores: &HashMap, +) -> Result<(Vec, Vec)> { + ensure!(seeds.len() <= SEED_LIMIT, "structural_seed_budget"); + let nodes = graph + .nodes + .iter() + .map(|n| (n.id, n)) + .collect::>(); + let seeds_set = seeds.iter().cloned().collect::>(); + let mut prior = BTreeSet::new(); + let mut successors = Vec::new(); + let mut steps = Vec::new(); + for seed in seeds { + let anchors = graph + .nodes + .iter() + .filter(|n| n.fragment_ids.contains(seed)) + .map(|n| n.id) + .collect::>(); + let relations = graph + .relations + .iter() + .filter(|e| { + anchors.contains(&e.source) + || anchors.contains(&e.target) + || e.occurrence_fragment_ids.contains(seed) + }) + .cloned() + .collect::>(); + let mut eligible = BTreeSet::new(); + let mut gaps = Vec::new(); + for edge in &relations { + ensure!( + edge.certainty == "Certain" && !edge.occurrence.source.trim().is_empty(), + "structural_edge_authority" + ); + for id in [edge.source, edge.target] { + let node = nodes.get(&id).context("structural_endpoint_missing")?; + if node.fragment_ids.is_empty() { + gaps.push(json!({"relation_id":edge.id,"node_id":id,"kind":"endpoint_outside_fragment_universe"})); + } + eligible.extend(node.fragment_ids.iter().cloned()); + } + if edge.occurrence_fragment_ids.is_empty() { + gaps.push( + json!({"relation_id":edge.id,"kind":"occurrence_outside_fragment_universe"}), + ); + } + eligible.extend(edge.occurrence_fragment_ids.iter().cloned()); + } + let excluded = seeds_set.union(&prior).cloned().collect::>(); + let mut ranked = eligible + .difference(&excluded) + .map(|id| { + let score = *scores.get(id).context("structural_score_missing")?; + ensure!(score.is_finite(), "structural_nonfinite_score"); + Ok((id.clone(), score)) + }) + .collect::>>()?; + ranked.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0))); + let retained = ranked + .into_iter() + .take(SUCCESSORS_PER_QUERY) + .map(|p| p.0) + .collect::>(); + prior.extend(retained.iter().cloned()); + successors.extend(retained.iter().cloned()); + steps.push(Step { + seed_fragment_id: seed.clone(), + anchors: anchors.into_iter().collect(), + relations, + eligible: eligible.into_iter().collect(), + excluded_before: excluded.into_iter().collect(), + retained_successors: retained, + boundary_gaps: gaps, + }); + } + ensure!( + successors.len() <= MAX_SUCCESSORS, + "structural_successor_budget" + ); + Ok((successors, steps)) +} + +pub fn execute(job_path: &Path) -> Result<()> { + let job_binding = bind_file(job_path, None)?; + let job: Job = read_bound_json(&job_binding)?; + let control = RunControl::new(&job.cancel_file)?; + let preparation: Etr1PreparationV1 = read_bound_json(&job.preparation)?; + let graph_inputs = fixed_inputs(&job, &preparation)?; + let build = build_identity()?; + ensure!(!build.source_dirty, "dirty_structural_binary"); + bind_file(&job.method.path, Some(&job.method.sha256))?; + let stage = stage_output_directory(&job.output)?; + if job.operation == "index_canary" { + ensure!( + preparation.authority == "synthetic_canary_only" && preparation.repositories.len() == 1, + "not_synthetic_canary" + ); + use codestory_contracts::workspace::SourceIndexPolicy; + use codestory_runtime::{ + RetrievalProcessDefaults, RetrievalRuntimeDefaults, RetrievalRuntimeOverrides, Runtime, + RuntimeProcessConfig, RuntimeRetrievalConfig, RuntimeRetrievalProfile, + }; + let repository = &preparation.repositories[0]; + let storage = job.output.join("codestory.db"); + // Publish the owned directory first because core pointers contain paths. + publish_output_directory(stage, &job.output)?; + let defaults = RetrievalProcessDefaults::new( + job.output.join("runtime"), + RetrievalRuntimeDefaults::default(), + ); + let retrieval = RuntimeRetrievalConfig::for_project_profile_with_process_defaults( + Some(&repository.local_root), + RuntimeRetrievalProfile::Local, + None, + &defaults, + &RetrievalRuntimeOverrides::default(), + ); + let runtime = + Runtime::new_with_process_config(RuntimeProcessConfig::new_with_retrieval_config( + retrieval, + SourceIndexPolicy::default(), + )); + runtime + .project_service() + .open_project_summary_with_storage_path(repository.local_root.clone(), storage.clone()) + .map_err(|e| anyhow::anyhow!(e.message))?; + runtime + .index_service() + .run_indexing_blocking_without_runtime_refresh( + codestory_contracts::api::IndexMode::Full, + ) + .map_err(|e| anyhow::anyhow!(e.message))?; + drop(runtime); + let pin = CoreReadSession::pin(&storage)?; + write_exclusive( + &job.output.join("prepared.json"), + &serialize_pretty(&json!({"project_root":repository.local_root, + "storage_path":storage,"core_pointer":pin.pointer(),"publication":repository.publication,"build":build}))?, + )?; + return Ok(()); + } + if job.operation == "export_graphs" { + let mut graphs = Vec::new(); + for repository in &preparation.repositories { + control.check()?; + let graph = graph_for( + repository, + &preparation.fragments, + job.graph_preparations + .get(&repository.repository_id) + .context("graph_preparation_missing")?, + )?; + authenticate_graph_input(&graph, &graph_inputs)?; + graphs.push(graph); + } + write_exclusive( + &stage.path().join("graphs.json"), + &serialize_pretty(&json!({"contract":"codestory.str1-graphs/v1", + "build":build,"job":job_binding,"preparation":job.preparation,"method":job.method,"graphs":graphs,"annotation_access":"not_accessed"}))?, + )?; + control.check()?; + return publish_output_directory(stage, &job.output); + } + ensure!(job.operation == "run", "unknown_structural_operation"); + let graph_binding = job.graphs.as_ref().context("graphs_missing")?; + let graph_value: Value = read_bound_json(graph_binding)?; + ensure!( + graph_value["build"] == serde_json::to_value(&build)? + && graph_value["preparation"] == serde_json::to_value(&job.preparation)?, + "graph_build_or_preparation_changed" + ); + let graphs: Vec = serde_json::from_value(graph_value["graphs"].clone())?; + let mut core_pins = Vec::new(); + for graph in &graphs { + authenticate_graph_input(graph, &graph_inputs)?; + let prepared: Value = read_bound_json(&graph.preparation)?; + let pin = CoreReadSession::pin(Path::new( + prepared["storage_path"] + .as_str() + .context("graph_storage_missing")?, + ))?; + ensure!( + serde_json::to_value(pin.pointer())? == graph.core_pointer + && pin.generation_path() == graph.core.path, + "graph_publication_drift" + ); + core_pins.push(pin); + bind_file(&graph.core.path, Some(&graph.core.sha256))?; + for file in &graph.source_bindings { + bind_file(&file.path, Some(&file.sha256))?; + } + } + let vector_binding = job.vectors.as_ref().context("vectors_missing")?; + let (_, vector_artifact, vectors) = + load_vector_artifact(&vector_binding.path, &vector_binding.sha256, &preparation)?; + let control_binding = job.control_run.as_ref().context("control_run_missing")?; + let frozen: Etr1RunManifestV1 = read_bound_json(control_binding)?; + ensure!( + frozen.preparation == job.preparation && frozen.fragment_vectors == *vector_binding, + "frozen_control_input_mismatch" + ); + let frozen_rows = frozen + .rows + .iter() + .map(read_bound_json::) + .collect::>>()?; + let runtime = SidecarRuntimeConfig::local(); + let events_path = + validate_isolated_state(job.state_root.as_ref().context("state_missing")?, &runtime)?; + codestory_cli::install_native_embedding_client_transport()?; + let mut residency = PerUserEmbeddingClient::for_runtime(&runtime)?.acquire_residency_lease()?; + let initial_engine = engine_receipt(residency.identity())?; + for key in ["model_digest", "ggml_build_identity"] { + ensure!( + initial_engine[key] == frozen.initial_engine[key] + && initial_engine[key] == vector_artifact.initial_engine[key], + "structural_cross_engine_mismatch:{key}" + ); + } + let client = ProductEmbeddingClient::new(&runtime); + let fragments = preparation + .fragments + .iter() + .map(|f| (f.fragment_id.clone(), f)) + .collect::>(); + let lexical = preparation + .repositories + .iter() + .map(|r| { + Ok(( + r.repository_id.clone(), + Etr1LexicalIndex::new( + r.fragment_ids + .iter() + .map(|id| fragments[id].source.as_str()), + )?, + )) + }) + .collect::>>()?; + let mut cursor = EventCursor::default(); + let mut batch_ordinal = 0; + let mut rows = Vec::new(); + for wording in &preparation.wordings { + control.check()?; + let started = Instant::now(); + let repository = preparation + .repositories + .iter() + .find(|r| r.repository_id == wording.repository_id) + .context("repository_missing")?; + let graph = graphs + .iter() + .find(|g| g.repository_id == wording.repository_id) + .context("graph_missing")?; + let phase = Instant::now(); + let (_, matches) = lexical[&repository.repository_id].search(&wording.question)?; + let seeds = natural_seed_prefix(&matches) + .iter() + .map(|m| repository.fragment_ids[m.rowid - 1].clone()) + .collect::>(); + ensure!(seeds == wording.seed_fragment_ids, "structural_seed_drift"); + let bm25 = phase.elapsed().as_nanos() as u64; + let mut authenticator = SourceAuthenticator::new(repository, &fragments); + let phase = Instant::now(); + for id in &seeds { + authenticator.authenticate(id)?; + } + let seed_auth = phase.elapsed().as_nanos() as u64; + let phase = Instant::now(); + let (query_vector, batches) = if let Some(seed) = seeds.first() { + let mut specs = vec![QuerySpec { + ordinal: 0, + seed_fragment_id: seed.clone(), + original_input: wording.question.clone(), + encoded_input: wording.question.clone(), + removed_trailing_source_lines: 0, + model_limit_rejections: 0, + }]; + let (encoded, batches) = encode_queries( + &control, + &client, + "structural", + &mut specs, + &[wording.question.clone()], + &[String::new()], + &events_path, + &mut cursor, + &mut batch_ordinal, + )?; + ensure!( + encoded[0].spec.encoded_input == wording.question + && encoded[0].spec.model_limit_rejections == 0, + "raw_query_changed" + ); + (encoded[0].vector.clone(), batches) + } else { + (Vec::new(), Vec::new()) + }; + let encoding = phase.elapsed().as_nanos() as u64; + let phase = Instant::now(); + let scores = if seeds.is_empty() { + Vec::new() + } else { + score_fragments(&query_vector, &repository.fragment_ids, &vectors)?.0 + }; + let score_map = repository + .fragment_ids + .iter() + .cloned() + .zip(scores.iter().copied()) + .collect::>(); + let (successors, steps) = frontier(graph, &seeds, &score_map)?; + let discovery = phase.elapsed().as_nanos() as u64; + let phase = Instant::now(); + let mut pool = seeds.clone(); + pool.extend(successors.iter().cloned()); + let legal = exact_legally_selectable_pool(&pool, repository, &fragments)?; + let mapping = phase.elapsed().as_nanos() as u64; + let phase = Instant::now(); + for id in &successors { + authenticator.authenticate(id)?; + } + let hydration = phase.elapsed().as_nanos() as u64; + let wall = started.elapsed().as_nanos() as u64; + let old = frozen_rows + .iter() + .position(|r| { + r["case_id"] == wording.case_id && r["phrasing_id"] == wording.phrasing_id + }) + .context("frozen_control_row_missing")?; + ensure!( + frozen_rows[old]["seed_fragment_ids"] == serde_json::to_value(&seeds)?, + "control_seed_mismatch" + ); + rows.push(json!({"case_id":wording.case_id,"phrasing_id":wording.phrasing_id,"group":wording.group,"repository_id":wording.repository_id, + "question_sha256":wording.question_sha256,"seed_fragment_ids":seeds,"control_row":frozen.rows[old], + "candidate":{"legally_selectable_pool":legal,"descriptor_pool":pool,"hydrated_pool":pool,"successors":successors, + "steps":steps,"query_input":wording.question,"query_vector":query_vector,"scores":scores,"batch_receipts":batches, + "source_authentication":authenticator.receipt,"timing":{"round_zero_bm25_ns":bm25,"seed_source_authentication_ns":seed_auth, + "query_encoding_ns":encoding,"structural_discovery_and_scoring_ns":discovery,"descriptor_mapping_ns":mapping, + "remaining_source_authentication_ns":hydration,"prepared_state_ns":wall,"unaccounted_ns":wall-(bm25+seed_auth+encoding+discovery+mapping+hydration)}}})); + } + control.check()?; + let final_engine = engine_receipt(&residency.revalidate()?)?; + ensure!( + initial_engine["server_instance_id"] == final_engine["server_instance_id"] + && initial_engine["load_generation"] == final_engine["load_generation"], + "structural_engine_drift" + ); + let events = fs::read(&events_path)?; + ensure!( + read_completed_events(&events_path)?.len() == cursor.completed_events, + "structural_completion_mismatch" + ); + write_exclusive(&stage.path().join("events.jsonl"), &events)?; + write_exclusive( + &stage.path().join("run.json"), + &serialize_pretty( + &json!({"contract":"codestory.str1-run/v1","build":build,"job":job_binding, + "preparation":job.preparation,"method":job.method,"graphs":graph_binding,"vectors":vector_binding,"control_run":control_binding, + "annotation_access":"not_accessed","experiment_status":"awaiting_validation","decision":"not_evaluated", + "initial_engine":initial_engine,"final_engine":final_engine,"events_sha256":sha256(&events),"rows":rows}), + )?, + )?; + control.check()?; + publish_output_directory(stage, &job.output) +} diff --git a/crates/codestory-bench/src/bin/codestory_str1.rs b/crates/codestory-bench/src/bin/codestory_str1.rs new file mode 100644 index 000000000..0276d3275 --- /dev/null +++ b/crates/codestory-bench/src/bin/codestory_str1.rs @@ -0,0 +1,32 @@ +//! The single benchmark-only structural frontier experiment. No product route. +#![allow(dead_code)] +use anyhow::{Result, ensure}; +use clap::Parser; +use std::path::PathBuf; +#[path = "codestory_proof_availability/build_provenance.rs"] +mod build_provenance; +#[path = "codestory_etr1/contract.rs"] +mod contract; +#[path = "codestory_etr1/control.rs"] +mod control; +#[path = "codestory_etr1/prepare.rs"] +mod prepare; +// Reuse native encoding, token accounting, exact source authentication and +// lexical reconstruction. The ETR command and algorithm remain unchanged. +#[path = "codestory_etr1/run.rs"] +mod run; +#[derive(Parser)] +struct Args { + #[arg(long)] + job: PathBuf, +} +fn main() -> Result<()> { + if std::env::args().nth(1).as_deref() == Some("internal-embedding-server") { + ensure!( + build_provenance::SOURCE_DIRTY.trim() == "false", + "dirty_str1_binary" + ); + return codestory_cli::run_native_embedding_server(); + } + run::str1::execute(&Args::parse().job) +} diff --git a/scripts/codestory-str1-canary.mjs b/scripts/codestory-str1-canary.mjs new file mode 100644 index 000000000..91ea9eea7 --- /dev/null +++ b/scripts/codestory-str1-canary.mjs @@ -0,0 +1,78 @@ +import assert from "node:assert/strict"; +import {execFileSync} from "node:child_process"; +import {mkdir,readFile,writeFile} from "node:fs/promises"; +import path from "node:path"; +import {randomUUID} from "node:crypto"; +import {fileURLToPath} from "node:url"; +import {executeRecorded,fileBinding,executionEnvironment} from "./lib/etr1-execution.mjs"; +import {strIdentity,readExecutionBinding} from "./lib/str1-execution.mjs"; +import {validateEtr1} from "./codestory-etr1-validate.mjs"; + +async function main() { + const config=JSON.parse(await readFile(process.argv[2],"utf8")),{root,binary,etrBinary,diagnostic,method,sourceRoot}=config; + await mkdir(root,{mode:0o700});const project=path.join(root,"repository");await mkdir(project,{mode:0o700}); + // Exercise the same command entry points used by the corpus owner, not + // imported approximations that conceal asynchronous module-loading cycles. + const command=async(name,script,input)=>{ + if(input.env)input={...input,env:executionEnvironment(input.env)}; + const file=path.join(root,`${name}-config.json`);await writeFile(file,JSON.stringify(input),{flag:"wx",mode:0o600}); + const stdout=execFileSync(process.execPath,[path.join(sourceRoot,script),file],{cwd:sourceRoot,encoding:"utf8",timeout:120_000}); + return stdout.trim().split("\n").map(line=>JSON.parse(line)); + }; + const supervise=async(name,input)=>{const output=await command(name,"scripts/lib/str1-execution.mjs",input); + assert.equal(output.length,2);return {request:output[0].prelaunch_request,binding:output[1],receipt:await readExecutionBinding(output[1])};}; + const source=Array.from({length:32},(_,i)=>`fn commonneedle_${i}() { ${i===0?Array.from({length:12},(_,j)=>`commonneedle_${j+18}();`).join(" "):`commonneedle_${(i+1)%32}();`}${i===0?" /* raremarker */":""} }${i===1?" /* a\u2028b\u2029c */":""}${i%3===0?"\r\n":"\n"}`).join(""); + await writeFile(path.join(project,"canary.rs"),source,{flag:"wx",mode:0o600}); + const git=(...args)=>execFileSync("git",["-C",project,"-c","core.hooksPath=/dev/null",...args],{stdio:"pipe"}); + git("init","--quiet");git("add","canary.rs");git("-c","user.name=STR canary","-c","user.email=canary@invalid.local","-c","commit.gpgsign=false","commit","--quiet","-m","freeze synthetic source"); + const prepared=path.join(root,"prepared");execFileSync(etrBinary,["prepare-canary","--project-root",project,"--output-dir",prepared],{stdio:"pipe",timeout:60_000}); + const preparation=await fileBinding(path.join(prepared,"preparation.json")),p=JSON.parse(await readFile(preparation.path,"utf8")); + const makeState=async name=>{const state=path.join(root,name),ipc=path.join(state,"ipc"),cache=path.join(state,"cache"); + await mkdir(state,{mode:0o700});await mkdir(ipc,{mode:0o700});await mkdir(cache,{mode:0o700});const nonce=`str1-${randomUUID()}`; + return {state,events:path.join(ipc,`${nonce}.events.jsonl`),env:{...process.env,CODESTORY_CACHE_ROOT:cache,CODESTORY_EMBED_ALLOW_CPU:"false",CODESTORY_EMBED_QUALIFICATION_DIR:ipc,CODESTORY_EMBED_QUALIFICATION_NONCE:nonce}};}; + const docs=await makeState("document-state"),vectorPath=path.join(docs.state,"vectors.json"); + const documents=await executeRecorded({role:"documents",authority:"synthetic_canary_only",executable:diagnostic, + args:["--input",p.embedding_input.path,"--input-sha256",p.embedding_input.sha256,"--state-root",docs.state,"--output",vectorPath], + inputs:[p.embedding_input.path],outputPaths:[vectorPath],eventsPath:docs.events,directory:path.join(root,"document-execution"),sourceRoot,env:docs.env}); + assert.equal(documents.receipt.experiment_status,"completed");const vectors=await fileBinding(vectorPath); + const ctl=await makeState("control-state"),controlDir=path.join(root,"control"),controlPath=path.join(controlDir,"run.json"),cancelFile=path.join(root,"cancel"); + const controlExecution=await executeRecorded({role:"paired_run",authority:"synthetic_canary_only",executable:etrBinary, + args:["run","--prepared",preparation.path,"--prepared-sha256",preparation.sha256,"--fragment-vectors",vectors.path,"--fragment-vectors-sha256",vectors.sha256, + "--document-execution",documents.binding.path,"--document-execution-sha256",documents.binding.sha256,"--state-root",ctl.state,"--output-dir",controlDir,"--cancel-file",cancelFile], + inputs:[preparation.path,vectors.path,documents.binding.path],outputPaths:[controlPath],eventsPath:ctl.events,directory:path.join(root,"control-execution"),sourceRoot,env:ctl.env,cancelFile}); + assert.equal(controlExecution.receipt.experiment_status,"completed");const controlRun=await fileBinding(controlPath); + const checkedControl=await validateEtr1({runBinding:controlRun,sourceRoot,executionBinding:controlExecution.binding,allowCanary:true}); + const cvPath=path.join(root,"control-validation.json");await writeFile(cvPath,JSON.stringify({contract:"codestory.etr1-validation/v1",authority:"synthetic_canary_only",experiment_status:"valid",decision:"not_evaluated",annotation_access:"not_accessed",run:controlRun,execution:controlExecution.binding,binary_sha256:checkedControl.run.build.binary_sha256}),{flag:"wx",mode:0o600}); + const controlValidation=await fileBinding(cvPath); + const writeJob=async(name,job)=>{const file=path.join(root,`${name}-job.json`);await writeFile(file,JSON.stringify(job),{flag:"wx",mode:0o600});return file;}; + const graphDir=path.join(root,"graph-core"),indexJob=await writeJob("index",{operation:"index_canary",preparation,method,cancel_file:cancelFile,output:graphDir}); + execFileSync(binary,["--job",indexJob],{cwd:sourceRoot,stdio:"pipe",timeout:120_000}); + const exportJob=await writeJob("export",{operation:"export_graphs",preparation,method,cancel_file:cancelFile,output:path.join(root,"graphs"),graph_preparations:{canary:await fileBinding(path.join(graphDir,"prepared.json"))}}); + const graphExecution=await supervise("graph-supervisor",{binary,jobPath:exportJob,directory:path.join(root,"graph-execution"),sourceRoot,env:process.env}); + assert.equal(graphExecution.receipt.experiment_status,"completed"); + const graphData=JSON.parse(await readFile(graphExecution.receipt.output.path,"utf8")); + assert.ok(graphData.graphs[0].relations.length>=12,"canary failed to exercise certain witnessed relationships"); + const query=await makeState("query-state"),runDir=path.join(root,"run"); + const runJob=await writeJob("run",{operation:"run",preparation,method,graphs:graphExecution.receipt.output,vectors,control_run:controlRun,state_root:query.state,cancel_file:cancelFile,output:runDir}); + const execution=await supervise("run-supervisor",{binary,jobPath:runJob,directory:path.join(root,"run-execution"),sourceRoot,env:query.env}); + assert.equal(execution.receipt.experiment_status,"completed"); + const validationPath=path.join(root,"validation.json"); + const [validation]=await command("validation","scripts/codestory-str1-validate.mjs",{execution:execution.binding,executionRequest:execution.request,graphExecution:graphExecution.binding,graphRequest:graphExecution.request,sourceRoot,controlValidation,controlSourceRoot:sourceRoot,reconstructionRoot:root,output:validationPath}); + assert.equal((await readExecutionBinding(validation)).experiment_status,"valid"); + const run=await readExecutionBinding(execution.receipt.output); + assert.ok(run.rows.some(row=>row.candidate.steps.some(step=>step.eligible.length>8)),"canary did not exercise overflow"); + assert.deepEqual(run.rows.map(r=>r.seed_fragment_ids.length),[16,1,0]); + const first=p.fragments[0],truth={authority:"synthetic_canary_only",cases:p.wordings.map(row=>({case_id:row.case_id,acceptable_sets:[{set_id:"first",required_relation_atoms:[],required_source_atoms:[{atom_id:"first",source_range:{path:first.path,content_digest:first.content_digest,byte_range:first.byte_range,line_range:first.line_range}}]}]}))}; + const truthPath=path.join(root,"annotations.json");await writeFile(truthPath,JSON.stringify(truth),{flag:"wx",mode:0o600}); + const annotations=await fileBinding(truthPath); + const [evaluation]=await command("evaluation","scripts/codestory-str1-evaluate.mjs",{validation,annotations,sourceRoot,output:path.join(root,"evaluation.json")}); + const evaluated=await readExecutionBinding(evaluation); + assert.deepEqual(evaluated.rows.map(row=>row.candidate.recall),[1,1,0]); + const args=["--test","scripts/tests/str1-evidence.test.mjs"]; + const stdout=execFileSync(process.execPath,args,{cwd:sourceRoot,encoding:"utf8",timeout:60_000}); + const hostileOutput=path.join(root,"hostile-output.json");await writeFile(hostileOutput,JSON.stringify({stdout}),{flag:"wx",mode:0o600}); + const hostilePath=path.join(root,"hostile.json");await writeFile(hostilePath,JSON.stringify({exit_code:0,args,node:await fileBinding(process.execPath),test_source:await fileBinding(path.join(sourceRoot,"scripts/tests/str1-evidence.test.mjs")),output:await fileBinding(hostileOutput)}),{flag:"wx",mode:0o600}); + const receiptPath=path.join(root,"receipt.json");await writeFile(receiptPath,JSON.stringify({contract:"codestory.str1-canary/v1",authority:"synthetic_canary_only",experiment_status:"valid",binary:await fileBinding(binary),analysis:await strIdentity(sourceRoot),validation,annotations,evaluated,hostile:await fileBinding(hostilePath)}),{flag:"wx",mode:0o600}); + console.log(JSON.stringify(await fileBinding(receiptPath))); +} +if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url))main().catch(e=>{console.error(e.stack);process.exitCode=1;}); diff --git a/scripts/codestory-str1-evaluate.mjs b/scripts/codestory-str1-evaluate.mjs new file mode 100644 index 000000000..e897f4fc9 --- /dev/null +++ b/scripts/codestory-str1-evaluate.mjs @@ -0,0 +1,59 @@ +import assert from "node:assert/strict"; +import {readFile,writeFile} from "node:fs/promises"; +import path from "node:path"; +import {fileURLToPath} from "node:url"; +import {validateStr1} from "./codestory-str1-validate.mjs"; +import {readExecutionBinding,fileBinding} from "./lib/str1-execution.mjs"; +import {evaluateArm,mean,percentile} from "./lib/etr1-evidence.mjs"; +import {gateOne,gateTwo,reproduceOracleFixtures} from "./codestory-etr1-evaluate.mjs"; + +export function aggregateStrRows(rows) { + const cases=[...new Set(rows.map(r=>r.case_id))].sort().map(id=>{ + const phrasings=rows.filter(r=>r.case_id===id),aggregate=name=>({recall:mean(phrasings.map(r=>r[name].recall)), + complete_set_rate:mean(phrasings.map(r=>Number(r[name].complete_source_set)))}); + return {case_id:id,group:phrasings[0].group,control:aggregate("control"),candidate:aggregate("candidate"), + control_incomplete_for_gain:phrasings.filter(r=>!r.control.complete_source_set).length>=2, + candidate_gained_atom:phrasings.filter(r=>r.candidate.reachable_atoms.some(a=>!r.control.reachable_atoms.includes(a))).length>=2}; + }); + const groups=[...new Set(cases.map(c=>c.group))].sort(),aggregate=name=>({mean_recall:mean(cases.map(c=>c[name].recall)), + complete_set_rate:mean(cases.map(c=>c[name].complete_set_rate)),groups:Object.fromEntries(groups.map(group=>[group, + {mean_recall:mean(cases.filter(c=>c.group===group).map(c=>c[name].recall)),complete_set_rate:mean(cases.filter(c=>c.group===group).map(c=>c[name].complete_set_rate))}]))}); + const control=aggregate("control"),candidate=aggregate("candidate"),sufficiency=gateOne(candidate),material=gateTwo(cases,control,candidate,sufficiency.pass); + const quality=sufficiency.pass&&material.pass; + const latency=quality?{status:"evaluated",p95_ns:percentile(rows.map(r=>r.candidate.prepared_state_ns),.95)}:{status:"not_evaluated",p95_ns:null}; + latency.pass=quality?latency.p95_ns<=1_250_000_000:null; + return {cases,aggregates:{control,candidate},gates:{sufficiency,material,latency}, + decision:quality&&latency.pass?"structural_frontier_selected":"no_frontier_selected", + next:quality&&latency.pass?"prepare_separate_selector_contract_only":"stop_automatic_packet_program"}; +} + +export async function evaluateStr1({validation,annotations,oracle,sourceRoot}) { + const prior=await readExecutionBinding(validation);assert.equal(prior.experiment_status,"valid");assert.equal(prior.annotation_access,"not_accessed"); + const checked=await validateStr1({execution:prior.execution,executionRequest:prior.execution_request,graphExecution:prior.graph_execution,graphRequest:prior.graph_request,controlValidation:prior.control_validation, + controlSourceRoot:prior.control_source_root,reconstructionRoot:prior.reconstruction_root,sourceRoot}); + assert.deepEqual(checked.validation,prior,"validation receipt changed"); + // First annotations read, after full native/control reconstruction. + const synthetic=checked.preparation.authority==="synthetic_canary_only"; + if(!synthetic)assert.equal(annotations.sha256,checked.preparation.annotations.sha256); + const truth=await readExecutionBinding(annotations); + assert.equal(truth.authority,synthetic?"synthetic_canary_only":"visible_development_only"); + const reproduction=synthetic?null:await reproduceOracleFixtures({oracle:await readExecutionBinding(oracle),preparation:checked.preparation,annotations:truth}); + const fragments=new Map(checked.preparation.fragments.map(f=>[f.fragment_id,f])); + const rows=checked.run.rows.map(row=>{ + const task=truth.cases.find(c=>c.case_id===row.case_id),repository=checked.preparation.repositories.find(r=>r.repository_id===row.repository_id); + assert.ok(task&&repository);const score=name=>({...evaluateArm(task,repository.fragment_ids.map(id=>fragments.get(id)),row[name].legally_selectable_pool,repository.base_serialized_bytes),prepared_state_ns:row[name].timing.prepared_state_ns}); + return {case_id:row.case_id,phrasing_id:row.phrasing_id,group:row.group,control:score("control"),candidate:score("candidate")}; + }); + if(synthetic)return {contract:"codestory.str1-evaluation/v1",authority:"synthetic_canary_only",experiment_status:"valid",decision:"not_evaluated",rows}; + assert.equal(rows.length,72);assert.equal(truth.questions_sha256,checked.preparation.fixed_inputs.questions.sha256); + for(const id of new Set(rows.map(r=>r.case_id)))assert.deepEqual(rows.filter(r=>r.case_id===id).map(r=>r.phrasing_id).sort(),["original","paraphrase_1","paraphrase_2"]); + return {contract:"codestory.str1-evaluation/v1",authority:"visible_development_frontier_only",experiment_status:"valid",packet_decision:"not_evaluated", + inputs:{validation,annotations,oracle},oracle_reproduction:reproduction,...aggregateStrRows(rows),rows}; +} +async function main() { + const config=JSON.parse(await readFile(process.argv[2],"utf8"));let report; + try{report=await evaluateStr1(config);}catch(error){report={contract:"codestory.str1-evaluation/v1",experiment_status:"invalid",decision:"not_evaluated",error:error.message};process.exitCode=1;} + await writeFile(config.output,JSON.stringify(report),{flag:"wx",mode:0o600});console.log(JSON.stringify(await fileBinding(config.output))); +} +if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)) + main().catch(error=>{console.error(error.stack);process.exitCode=1;}); diff --git a/scripts/codestory-str1-validate.mjs b/scripts/codestory-str1-validate.mjs new file mode 100644 index 000000000..6bca6bc10 --- /dev/null +++ b/scripts/codestory-str1-validate.mjs @@ -0,0 +1,121 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { readFile, writeFile, mkdtemp } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { validateStrExecution, readExecutionBinding, fileBinding, strIdentity, assertStrInputs } from "./lib/str1-execution.mjs"; +import { structuralFrontier, validateStructuralGraph } from "./lib/str1-evidence.mjs"; +import { authenticateFragment, f32Dot, sha256, validateVector } from "./lib/etr1-evidence.mjs"; +import { parseEvents, validateEngine, validateEtr1 } from "./codestory-etr1-validate.mjs"; + +export async function validateStr1({execution,executionRequest,graphExecution,graphRequest,sourceRoot,controlValidation,controlSourceRoot,reconstructionRoot}) { + const external=await validateStrExecution(execution,sourceRoot,executionRequest); + const exported=await validateStrExecution(graphExecution,sourceRoot,graphRequest); + const run=await readExecutionBinding(external.receipt.output),graphOutput=await readExecutionBinding(exported.receipt.output); + assert.equal(run.contract,"codestory.str1-run/v1");assert.equal(graphOutput.contract,"codestory.str1-graphs/v1"); + assert.equal(run.annotation_access,"not_accessed");assert.equal(graphOutput.annotation_access,"not_accessed"); + assert.equal(run.experiment_status,"awaiting_validation");assert.equal(run.decision,"not_evaluated"); + assert.deepEqual(run.job,external.request.job);assert.deepEqual(graphOutput.job,exported.request.job); + assert.deepEqual(run.graphs,exported.receipt.output); + assert.deepEqual(run.preparation,external.job.preparation);assert.deepEqual(graphOutput.preparation,run.preparation); + for(const item of [run,graphOutput]) { + assert.equal(item.build.source_commit,external.request.analysis.source_commit); + assert.equal(item.build.source_tree,external.request.analysis.source_tree); + assert.equal(item.build.source_dirty,false);assert.equal(item.build.binary_sha256,external.request.binary.sha256); + assert.deepEqual(item.method,external.job.method); + } + const old=await readExecutionBinding(controlValidation); + assert.equal(old.experiment_status,"valid");assert.equal(old.decision,"not_evaluated"); + const controls=await validateEtr1({runBinding:old.run,sourceRoot:controlSourceRoot,executionBinding:old.execution, + allowCanary:old.authority==="synthetic_canary_only"}); + assert.deepEqual(run.control_run,old.run);assert.deepEqual(run.preparation,controls.run.preparation); + assert.deepEqual(run.vectors,controls.run.fragment_vectors); + assertStrInputs(external.job,controls.preparation,controlValidation); + // Re-export through the pinned read-session path; don't trust graph JSON + // merely because someone also updated its digest. + const reconstruction=await mkdtemp(path.join(reconstructionRoot,"str1-graph-reconstruction-")); + const replayJob={...exported.job,output:path.join(reconstruction,"output"),cancel_file:path.join(reconstruction,"cancel")}; + const replayPath=path.join(reconstruction,"job.json");await writeFile(replayPath,JSON.stringify(replayJob),{flag:"wx",mode:0o600}); + execFileSync(external.request.binary.path,["--job",replayPath],{cwd:sourceRoot,env:external.request.environment,timeout:120_000,stdio:"pipe"}); + const rebuilt=JSON.parse(await readFile(path.join(replayJob.output,"graphs.json"),"utf8")); + assert.deepEqual(rebuilt.graphs,graphOutput.graphs,"native graph reconstruction differs"); + graphOutput.graphs.forEach(validateStructuralGraph); + const preparation=controls.preparation,fragments=new Map(preparation.fragments.map(f=>[f.fragment_id,f])); + const vectors=await readExecutionBinding(run.vectors),vectorMap=new Map(vectors.records.map(r=>[r.id,r.vector])); + const eventsBytes=await readFile(external.receipt.events.path); + assert.equal(sha256(eventsBytes),run.events_sha256); + const events=parseEvents(eventsBytes);let eventOrdinal=0,totalWall=0; + validateEngine(run.initial_engine);validateEngine(run.final_engine); + for(const key of ["server_instance_id","load_generation","model_digest","ggml_build_identity"]) + assert.deepEqual(run.initial_engine[key],run.final_engine[key]); + for(const key of ["model_digest","ggml_build_identity"]) { + assert.deepEqual(run.initial_engine[key],controls.run.initial_engine[key]); + assert.deepEqual(run.initial_engine[key],vectors.initial_engine[key]); + } + assert.equal(run.rows.length,preparation.wordings.length); + for(let index=0;indexr.repository_id===row.repository_id); + const graph=graphOutput.graphs.find(g=>g.repository_id===row.repository_id); + assert.equal(candidate.query_input,wording.question,"raw query changed"); + const seeded=wording.seed_fragment_ids.length>0; + assert.equal(candidate.batch_receipts.length,seeded?1:0); + assert.equal(candidate.scores.length,seeded?repository.fragment_ids.length:0); + if(seeded) { + validateVector(candidate.query_vector); + candidate.scores.forEach((score,i)=>assert.ok(Math.abs(score-f32Dot(candidate.query_vector,vectorMap.get(repository.fragment_ids[i])))<=2e-6,"structural similarity differs")); + const batch=candidate.batch_receipts[0],event=events[eventOrdinal]; + assert.ok(event,"native completion omitted"); + assert.equal(batch.global_batch_ordinal,eventOrdinal++);assert.equal(batch.arm,"structural"); + assert.deepEqual(batch.query_ordinals,[0]);assert.deepEqual(batch.input_sha256,[sha256(wording.question)]); + assert.equal(batch.completed_tokens,Number(event.details.completed_tokens)); + assert.equal(batch.qualification_native_completion_sequence,Number(event.details.native_completion_sequence)); + assert.equal(batch.qualification_server_event_sequence,event.server_event_sequence); + assert.equal(batch.qualification_request_id_sha256,sha256(event.details.request_id)); + const baseline=row.control.batch_receipts[0]; + assert.equal(batch.completed_tokens*baseline.query_ordinals.length,baseline.completed_tokens,"raw query tokenization changed"); + } else assert.deepEqual(candidate.query_vector,[]); + const scoreMap=new Map(repository.fragment_ids.map((id,i)=>[id,candidate.scores[i]])); + const expected=structuralFrontier(graph,wording.seed_fragment_ids,scoreMap); + assert.deepEqual(candidate.steps,expected.steps,"structural one-hop receipts differ"); + assert.deepEqual(candidate.successors,expected.successors,"structural successor selection differs"); + const pool=[...wording.seed_fragment_ids,...expected.successors]; + assert.ok(pool.length<=144&&new Set(pool).size===pool.length); + assert.deepEqual(candidate.descriptor_pool,pool);assert.deepEqual(candidate.hydrated_pool,pool); + assert.deepEqual(candidate.legally_selectable_pool,pool.filter(id=>repository.base_serialized_bytes+fragments.get(id).serialized_row_bytes<=16384)); + const sourceFiles=new Map();let sourceBytes=0; + for(const id of pool) { + const fragment=fragments.get(id);assert.equal(fragment.project_id,repository.project_id); + if(!sourceFiles.has(fragment.path))sourceFiles.set(fragment.path,await readFile(path.join(repository.local_root,fragment.path))); + authenticateFragment(fragment,sourceFiles.get(fragment.path));sourceBytes+=Buffer.byteLength(fragment.source); + } + assert.deepEqual(candidate.source_authentication.authenticated_fragment_ids,pool); + assert.equal(candidate.source_authentication.fragment_source_bytes,sourceBytes); + assert.equal(candidate.source_authentication.filesystem_bytes_read,[...sourceFiles.values()].reduce((s,b)=>s+b.length,0)); + assert.deepEqual(candidate.source_authentication.file_digests,Object.fromEntries([...sourceFiles].map(([p,b])=>[p,sha256(b)]))); + const {prepared_state_ns,unaccounted_ns,...phases}=candidate.timing; + for(const value of Object.values(candidate.timing))assert.ok(Number.isSafeInteger(value)&&value>=0,"invalid timing interval"); + assert.equal(Object.values(phases).reduce((a,b)=>a+b,0)+unaccounted_ns,prepared_state_ns); + totalWall+=prepared_state_ns; + } + assert.equal(eventOrdinal,events.length);assert.ok(totalWall<=external.receipt.wall_ns,"request wall exceeds process wall"); + return {run,preparation,graphs:graphOutput.graphs,validation:{contract:"codestory.str1-validation/v1", + experiment_status:"valid",decision:"not_evaluated",annotation_access:"not_accessed",authority:preparation.authority, + execution,execution_request:executionRequest,graph_execution:graphExecution,graph_request:graphRequest,control_validation:controlValidation,control_source_root:controlSourceRoot, + reconstruction_root:reconstructionRoot,run:external.receipt.output,analysis:await strIdentity(sourceRoot), + prepared_state_ns:totalWall,execution_wall_ns:external.receipt.wall_ns,outer_remainder_ns:external.receipt.wall_ns-totalWall}}; +} +async function main() { + const config=JSON.parse(await readFile(process.argv[2],"utf8"));let report; + try{report=(await validateStr1(config)).validation;}catch(error){report={contract:"codestory.str1-validation/v1",experiment_status:"invalid",decision:"not_evaluated",error:error.message};process.exitCode=1;} + await writeFile(config.output,JSON.stringify(report),{flag:"wx",mode:0o600});console.log(JSON.stringify(await fileBinding(config.output))); +} +if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)) + main().catch(error=>{console.error(error.stack);process.exitCode=1;}); diff --git a/scripts/lib/str1-evidence.mjs b/scripts/lib/str1-evidence.mjs new file mode 100644 index 000000000..191eb9d40 --- /dev/null +++ b/scripts/lib/str1-evidence.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; + +export function validateStructuralGraph(graph) { + const nodes = new Map(graph.nodes.map((node) => [node.id, node])); + assert.equal(nodes.size, graph.nodes.length, "duplicate node identity"); + const edges = new Set(); + for (const edge of graph.relations) { + assert.ok(!edges.has(edge.id), "duplicate relationship identity"); edges.add(edge.id); + assert.equal(edge.certainty, "Certain", "uncertain relationship"); + assert.ok(nodes.has(edge.source) && nodes.has(edge.target), "missing effective endpoint"); + const occurrence = edge.occurrence; + assert.ok(occurrence && occurrence.path && occurrence.source.trim(), "missing positive occurrence"); + assert.match(occurrence.content_digest, /^[0-9a-f]{64}$/u); + assert.ok(occurrence.start_line > 0 && occurrence.end_line >= occurrence.start_line); + } +} + +export function structuralFrontier(graph, seeds, scores) { + validateStructuralGraph(graph); + assert.ok(seeds.length <= 16 && new Set(seeds).size === seeds.length, "seed budget/identity"); + const nodes = new Map(graph.nodes.map((node) => [node.id, node])); + const prior = new Set(), steps = []; + for (const seed of seeds) { + const anchors = new Set(graph.nodes.filter((node) => node.fragment_ids.includes(seed)).map((node) => node.id)); + const relations = graph.relations.filter((edge) => anchors.has(edge.source) || anchors.has(edge.target) + || edge.occurrence_fragment_ids.includes(seed)); + const eligible = new Set(), gaps = []; + for (const edge of relations) { + for (const nodeId of [edge.source, edge.target]) { + const node = nodes.get(nodeId); + if (!node.fragment_ids.length) gaps.push({ relation_id: edge.id, node_id: nodeId, kind: "endpoint_outside_fragment_universe" }); + node.fragment_ids.forEach((id) => eligible.add(id)); + } + if (!edge.occurrence_fragment_ids.length) gaps.push({ relation_id: edge.id, kind: "occurrence_outside_fragment_universe" }); + edge.occurrence_fragment_ids.forEach((id) => eligible.add(id)); + } + const excluded = [...new Set([...seeds, ...prior])].sort(); + const ranked = [...eligible].filter((id) => !excluded.includes(id)); + for (const id of ranked) assert.ok(Number.isFinite(scores.get(id)), "missing/nonfinite similarity"); + ranked.sort((a, b) => scores.get(b) - scores.get(a) || (a < b ? -1 : a > b ? 1 : 0)); + const selected = ranked.slice(0, 8); + selected.forEach((id) => prior.add(id)); + steps.push({ seed_fragment_id: seed, anchors: [...anchors].sort((a, b) => a - b), + relations, eligible: [...eligible].sort(), excluded_before: excluded, + retained_successors: selected, boundary_gaps: gaps }); + } + return { successors: [...prior], steps }; +} diff --git a/scripts/lib/str1-execution.mjs b/scripts/lib/str1-execution.mjs new file mode 100644 index 000000000..4f73e48de --- /dev/null +++ b/scripts/lib/str1-execution.mjs @@ -0,0 +1,143 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { mkdir, readFile, writeFile, realpath, stat } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { analysisIdentity, executionEnvironment, fileBinding, readExecutionBinding } from "./etr1-execution.mjs"; +import {sha256} from "./etr1-evidence.mjs"; +export { fileBinding, readExecutionBinding }; +export const STR_FIXED=Object.freeze({method:"0902f8f8f6771fce5c6addf09bbdef061fd3706be0da050c32baead80fb171fc", + preparation:"30b84d4d848f96bd4fe799f2e0f28b9114971da0e47bf98ebe54fe36242199fd", + vectors:"7f604b30b823066bd5b0ed71106d10577c28495abd270444bc8ad5b7a63cb70a", + control:"c14da697d03707c0096f5f2fd7a97bff2ab5b4a6f9326c4a9a03da2066d545f2", + validation:"dd15c5842b68857c8c684b680cf3d1e307cd4b1a874736441c594490723e7145", + graph_inputs:"668c990ee29b25a4bab0cb03e048d70eebd8ffe3dd62317d69b2e58b912a2c9f"}); +export function assertStrInputs(job,preparation,controlValidation) { + assert.equal(job.method.sha256,STR_FIXED.method,"unregistered structural method"); + assert.ok(["synthetic_canary_only","visible_development_frontier_only"].includes(preparation.authority)); + if(preparation.authority==="synthetic_canary_only")return; + assert.equal(job.preparation.sha256,STR_FIXED.preparation,"unregistered ETR preparation"); + assert.equal(job.graph_inputs?.sha256,STR_FIXED.graph_inputs,"unregistered graph inputs"); + if(job.vectors)assert.equal(job.vectors.sha256,STR_FIXED.vectors,"unregistered fragment vectors"); + if(job.control_run)assert.equal(job.control_run.sha256,STR_FIXED.control,"unregistered control run"); + if(controlValidation)assert.equal(controlValidation.sha256,STR_FIXED.validation,"unregistered control validation"); +} +export function assertStrRequest(request,observedBinding,expectedBinding) { + assert.ok(expectedBinding,"independently frozen request required"); + assert.deepEqual(observedBinding,expectedBinding,"execution request differs from prelaunch freeze"); + assert.equal(request.contract,"codestory.str1-execution-request/v1"); + assert.deepEqual(request.args,["--job",request.job.path],"execution argv changed"); + assert.equal(request.context_sha256,sha256(JSON.stringify({cwd:request.cwd,environment:request.environment})),"execution context changed"); +} +async function authenticateGraphInputs(job,preparation) { + if(preparation.authority==="synthetic_canary_only")return; + const inputs=await readExecutionBinding(job.graph_inputs); + assert.equal(inputs.contract,"codestory.str1-graph-inputs/v1"); + assert.deepEqual(inputs.repositories.map(r=>r.repository_id).sort(),preparation.repositories.map(r=>r.repository_id).sort()); + for(const input of inputs.repositories) { + for(const file of [input.preparation,input.core,input.pointer])assert.deepEqual(await fileBinding(file.path),file); + const wal=await stat(`${input.core.path}-wal`).catch(e=>{if(e.code!=="ENOENT")throw e;return null;}); + assert.ok(!wal||wal.size===0,"unbound graph WAL"); + if(job.operation==="export_graphs")assert.deepEqual(job.graph_preparations[input.repository_id],input.preparation); + } +} +export const STR_FILES = ["scripts/lib/str1-evidence.mjs", "scripts/lib/str1-execution.mjs", + "scripts/codestory-str1-validate.mjs", "scripts/codestory-str1-evaluate.mjs", "scripts/codestory-str1-canary.mjs", "scripts/tests/str1-evidence.test.mjs"]; +export async function strIdentity(root) { + return { ...await analysisIdentity(root), structural_files: await Promise.all(STR_FILES.map(f=>fileBinding(path.join(root,f)))) }; +} +export async function validateStrExecution(binding, sourceRoot, expectedRequest) { + const receipt=await readExecutionBinding(binding), request=await readExecutionBinding(receipt.request); + assertStrRequest(request,receipt.request,expectedRequest); + assert.equal(receipt.contract,"codestory.str1-execution/v1"); + assert.equal(receipt.experiment_status,"completed"); assert.equal(receipt.exit_code,0); + assert.equal(receipt.signal,null); assert.equal(receipt.cancelled,false); + assert.equal(request.deadline_ms,1_800_000); + assert.equal(request.cwd,await realpath(sourceRoot)); + assert.deepEqual(request.environment,executionEnvironment(request.environment)); + assert.deepEqual(request.analysis,await strIdentity(sourceRoot)); + assert.deepEqual(request.binary,await fileBinding(request.binary.path)); + const job=await readExecutionBinding(request.job); + for(const input of request.inputs) assert.deepEqual(input,await fileBinding(input.path)); + for(const input of [job.preparation,job.method,job.graph_inputs,job.graphs,job.vectors,job.control_run,...Object.values(job.graph_preparations??{})].filter(Boolean)) + assert.ok(request.inputs.some(x=>JSON.stringify(x)===JSON.stringify(input)),"unbound execution input"); + assert.deepEqual(receipt.output,await fileBinding(receipt.output.path)); + assert.equal(receipt.output.path,path.join(job.output,job.operation==="run"?"run.json":"graphs.json")); + for(const stream of [receipt.stdout,receipt.stderr,receipt.events].filter(Boolean)) assert.deepEqual(stream,await fileBinding(stream.path)); + if(job.operation==="run") assert.ok(receipt.events,"native events missing"); + const preparation=await readExecutionBinding(job.preparation); + assertStrInputs(job,preparation); + await authenticateGraphInputs(job,preparation); + if(preparation.authority!=="synthetic_canary_only") { + await validateStrCanary(request.canary,sourceRoot,request.binary); + } + assert.ok(Number.isSafeInteger(receipt.wall_ns)&&receipt.wall_ns>0); + return {request,receipt,job}; +} + +async function validateStrCanary(binding,sourceRoot,binary) { + assert.ok(binding,"real canary required before corpus execution"); + const canary=await readExecutionBinding(binding); + assert.equal(canary.contract,"codestory.str1-canary/v1");assert.equal(canary.authority,"synthetic_canary_only"); + assert.equal(canary.experiment_status,"valid");assert.deepEqual(canary.analysis,await strIdentity(sourceRoot)); + assert.deepEqual(canary.binary,binary); + const validation=await readExecutionBinding(canary.validation); + assert.equal(validation.authority,"synthetic_canary_only");assert.equal(validation.experiment_status,"valid"); + const {evaluateStr1}=await import("../codestory-str1-evaluate.mjs"); + const evaluated=await evaluateStr1({validation:canary.validation,annotations:canary.annotations,sourceRoot}); + assert.deepEqual(evaluated,canary.evaluated,"canary was not evaluated by the real pipeline"); + assert.deepEqual(evaluated.rows.map(row=>row.candidate.recall),[1,1,0]); + const hostile=await readExecutionBinding(canary.hostile); + assert.equal(hostile.exit_code,0);assert.deepEqual(hostile.args,["--test","scripts/tests/str1-evidence.test.mjs"]); + assert.deepEqual(hostile.test_source,await fileBinding(path.join(sourceRoot,"scripts/tests/str1-evidence.test.mjs"))); + assert.deepEqual(hostile.node,canary.analysis.node);await readExecutionBinding(hostile.output); +} + +export async function executeStr({binary,jobPath,directory,sourceRoot,env,canary=null}) { + const jobBinding=await fileBinding(jobPath),job=await readExecutionBinding(jobBinding); + const preparation=await readExecutionBinding(job.preparation); + assertStrInputs(job,preparation); + await authenticateGraphInputs(job,preparation); + if(preparation.authority!=="synthetic_canary_only") { + await validateStrCanary(canary,sourceRoot,await fileBinding(binary)); + } + await mkdir(directory,{mode:0o700}); + const request={contract:"codestory.str1-execution-request/v1",job:jobBinding,canary, + binary:await fileBinding(binary),cwd:await realpath(sourceRoot),environment:executionEnvironment(env), + analysis:await strIdentity(sourceRoot),deadline_ms:1_800_000, + inputs:await Promise.all([job.preparation,job.method,job.graph_inputs,job.graphs,job.vectors,job.control_run,...Object.values(job.graph_preparations??{})] + .filter(Boolean).map(async binding=>{assert.deepEqual(await fileBinding(binding.path),binding);return binding;}))}; + request.args=["--job",jobPath]; + request.context_sha256=sha256(JSON.stringify({cwd:request.cwd,environment:request.environment})); + const requestPath=path.join(directory,"request.json");await writeFile(requestPath,JSON.stringify(request),{flag:"wx",mode:0o600}); + const requestBinding=await fileBinding(requestPath); + // Emitted before spawn so the owner can retain it independently of results. + console.log(JSON.stringify({prelaunch_request:requestBinding})); + const started=process.hrtime.bigint(),child=spawn(binary,request.args,{cwd:request.cwd,env:request.environment,stdio:["ignore","pipe","pipe"]}); + const stdout=[],stderr=[];child.stdout.on("data",b=>stdout.push(b));child.stderr.on("data",b=>stderr.push(b)); + let cancelled=false,killTimer; + const cancel=()=>{cancelled=true;void writeFile(job.cancel_file,"cancel\n",{flag:"wx",mode:0o600}).catch(()=>{}); + killTimer??=setTimeout(()=>child.kill("SIGKILL"),5000);}; + process.once("SIGINT",cancel);process.once("SIGTERM",cancel);const timer=setTimeout(cancel,request.deadline_ms); + const terminal=await new Promise(resolve=>{child.once("error",e=>resolve({exit_code:null,signal:null,error:e.message})); + child.once("close",(code,signal)=>resolve({exit_code:code,signal}));}); + clearTimeout(timer);clearTimeout(killTimer);process.removeListener("SIGINT",cancel);process.removeListener("SIGTERM",cancel); + const wall_ns=Number(process.hrtime.bigint()-started); + for(const [name,bytes]of [["stdout",stdout],["stderr",stderr]])await writeFile(path.join(directory,`${name}.log`),Buffer.concat(bytes),{flag:"wx",mode:0o600}); + const output=await fileBinding(path.join(job.output,job.operation==="run"?"run.json":"graphs.json")).catch(()=>null); + const events=job.operation==="run"?await fileBinding(path.join(job.output,"events.jsonl")).catch(()=>null):null; + const completed=terminal.exit_code===0&&!terminal.signal&&!cancelled&&output&&(job.operation!=="run"||events); + const receipt={contract:"codestory.str1-execution/v1",experiment_status:completed?"completed":"invalid",decision:"not_evaluated", + request:requestBinding,...terminal,cancelled,wall_ns,output,events, + stdout:await fileBinding(path.join(directory,"stdout.log")),stderr:await fileBinding(path.join(directory,"stderr.log"))}; + const receiptPath=path.join(directory,"receipt.json");await writeFile(receiptPath,JSON.stringify(receipt),{flag:"wx",mode:0o600}); + return {receipt,request:requestBinding,binding:await fileBinding(receiptPath)}; +} +if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)) { + // Finish module evaluation before loading the validator, which imports this + // module too. Awaiting that cycle at top level deadlocks the corpus gate. + readFile(process.argv[2],"utf8").then(JSON.parse).then(executeStr).then(result=>{ + console.log(JSON.stringify(result.binding)); + if(result.receipt.experiment_status!=="completed")process.exitCode=1; + }).catch(error=>{console.error(error.stack);process.exitCode=1;}); +} diff --git a/scripts/tests/str1-evidence.test.mjs b/scripts/tests/str1-evidence.test.mjs new file mode 100644 index 000000000..a070ab1f6 --- /dev/null +++ b/scripts/tests/str1-evidence.test.mjs @@ -0,0 +1,79 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { structuralFrontier, validateStructuralGraph } from "../lib/str1-evidence.mjs"; +import { STR_FIXED, assertStrInputs, assertStrRequest } from "../lib/str1-execution.mjs"; +import { sha256 } from "../lib/etr1-evidence.mjs"; + +const fixture = () => ({ + nodes: [ + { id: 1, fragment_ids: ["seed"] }, + ...Array.from({ length: 12 }, (_, i) => ({ id: i + 2, fragment_ids: [`f${String(i).padStart(2, "0")}`] })), + { id: 90, fragment_ids: ["second-hop"] }, { id: 91, fragment_ids: [] }, + ], + relations: [ + ...Array.from({ length: 12 }, (_, i) => ({ id: i + 20, source: i % 2 ? i + 2 : 1, + target: i % 2 ? 1 : i + 2, certainty: "Certain", occurrence: { path: "a.rs", start_line: 1, end_line: 1, + content_digest: "a".repeat(64), source: "call();\n" }, occurrence_fragment_ids: ["seed"] })), + { id: 80, source: 2, target: 90, certainty: "Certain", occurrence: { path: "a.rs", start_line: 2, end_line: 2, + content_digest: "a".repeat(64), source: "later();\n" }, occurrence_fragment_ids: ["f00"] }, + { id: 81, source: 1, target: 91, certainty: "Certain", occurrence: { path: "a.rs", start_line: 1, end_line: 1, + content_digest: "a".repeat(64), source: "call();\n" }, occurrence_fragment_ids: ["seed"] }, + ], +}); + +test("one-hop discovery is directed in provenance, symmetric in eligibility, and bounded after exclusions", () => { + const graph = fixture(); + const scores = new Map(graph.nodes.flatMap((node) => node.fragment_ids).map((id) => [id, 1])); + const first = structuralFrontier(graph, ["seed"], scores); + assert.deepEqual(first.successors, Array.from({ length: 8 }, (_, i) => `f0${i}`)); + assert.ok(!first.successors.includes("second-hop")); + assert.ok(!first.steps[0].eligible.includes("second-hop"), "an observed edge must not promote its remote endpoint to a new traversal root"); + assert.ok(first.steps[0].relations.some((edge) => edge.source !== 1)); + assert.ok(first.steps[0].boundary_gaps.some((gap) => gap.node_id === 91)); + const paired = structuralFrontier(graph, ["seed", "f00"], scores); + assert.equal(new Set(paired.successors).size, paired.successors.length); + assert.ok(!paired.successors.includes("f00")); + assert.ok(paired.successors.includes("second-hop"), "only an original second seed opens its neighborhood"); +}); + +test("uncertain, unwitnessed, duplicate and nonfinite graph inputs cannot gain authority", () => { + for (const mutate of [ + (g) => { g.relations[0].certainty = "Probable"; }, + (g) => { g.relations[0].occurrence = null; }, + (g) => { g.relations[0].target = 9999; }, + (g) => { g.relations.push(g.relations[0]); }, + (g) => { g.relations[0].occurrence.source = ""; }, + ]) { const graph = fixture(); mutate(graph); assert.throws(() => validateStructuralGraph(graph)); } + assert.throws(() => structuralFrontier(fixture(), ["seed"], new Map([["f00", NaN]]))); +}); + +test("empty and naturally underfilled frontiers are retained; IDs alone break score ties", () => { + assert.deepEqual(structuralFrontier(fixture(), [], new Map()).successors, []); + const graph = fixture(); graph.relations = graph.relations.slice(0, 2); + const scores = new Map([["seed", 0], ["f00", 0.4], ["f01", 0.8]]); + assert.deepEqual(structuralFrontier(graph, ["seed"], scores).successors, ["f01", "f00"]); + assert.deepEqual(structuralFrontier(graph, ["seed"], new Map([["seed", 0], ["f00", 1], ["f01", 1]])).successors, + ["f00", "f01"]); +}); + +test("frozen experiment inputs cannot be replaced by self-declared method or core identities", () => { + const job={method:{sha256:STR_FIXED.method},preparation:{sha256:STR_FIXED.preparation},vectors:{sha256:STR_FIXED.vectors}, + control_run:{sha256:STR_FIXED.control},graph_inputs:{sha256:STR_FIXED.graph_inputs}}; + const preparation={authority:"visible_development_frontier_only"}; + assertStrInputs(job,preparation,{sha256:STR_FIXED.validation}); + for(const field of Object.keys(job)) {const changed=structuredClone(job);changed[field].sha256="f".repeat(64); + assert.throws(()=>assertStrInputs(changed,preparation,{sha256:STR_FIXED.validation}));} + assert.throws(()=>assertStrInputs(job,preparation,{sha256:"f".repeat(64)})); +}); + +test("request contract, argv, context and independently frozen request binding are required", () => { + const job={path:"/external/job.json",sha256:"1".repeat(64),bytes:12},binding={path:"/external/request.json",sha256:"2".repeat(64),bytes:99}; + const request={contract:"codestory.str1-execution-request/v1",job,cwd:"/source",environment:{LANG:"C"},args:["--job",job.path]}; + request.context_sha256=sha256(JSON.stringify({cwd:request.cwd,environment:request.environment})); + assertStrRequest(request,binding,binding); + for(const mutate of [r=>{r.contract="fake";},r=>{r.environment.LANG="changed";},r=>{r.args=["--job","/other"]; }]) { + const changed=structuredClone(request);mutate(changed);assert.throws(()=>assertStrRequest(changed,binding,binding)); + } + assert.throws(()=>assertStrRequest(request,{...binding,sha256:"3".repeat(64)},binding)); + assert.throws(()=>assertStrRequest(request,binding,undefined)); +});