diff --git a/crates/codestory-bench/Cargo.toml b/crates/codestory-bench/Cargo.toml index d582a563d..be2cb39b2 100644 --- a/crates/codestory-bench/Cargo.toml +++ b/crates/codestory-bench/Cargo.toml @@ -26,6 +26,10 @@ sha2 = { workspace = true } name = "codestory-proof-availability" path = "src/bin/codestory_proof_availability.rs" +[[bin]] +name = "codestory-witness-seam" +path = "src/bin/codestory_witness_seam.rs" + [dev-dependencies] codestory-retrieval = { workspace = true, features = ["benchmark-support"] } criterion = { workspace = true } diff --git a/crates/codestory-bench/examples/codestory_embedding_diagnostic.rs b/crates/codestory-bench/examples/codestory_embedding_diagnostic.rs new file mode 100644 index 000000000..43c5ec380 --- /dev/null +++ b/crates/codestory-bench/examples/codestory_embedding_diagnostic.rs @@ -0,0 +1,388 @@ +//! Post-failure measurement helper, never a product or release qualification route. +//! Uses the unchanged product encoder with an isolated native server namespace. + +use anyhow::{Context, Result, bail, ensure}; +use clap::Parser; +use codestory_retrieval::{ + EmbeddingEngineIdentity, PerUserEmbeddingClient, ProductEmbeddingClient, SidecarRuntimeConfig, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::HashSet; +use std::fs; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +#[path = "../src/bin/codestory_proof_availability/build_provenance.rs"] +mod build_provenance; + +const INPUT_CONTRACT: &str = "codestory.embedding-diagnostic-input/v1"; +const MAX_INPUT_BYTES: u64 = 32 * 1024 * 1024; +const BATCH_SIZE: usize = 16; + +#[derive(Parser)] +struct Args { + #[arg(long)] + input: PathBuf, + #[arg(long)] + input_sha256: String, + /// Existing private directory containing cache/ and ipc/; output stays here. + #[arg(long)] + state_root: PathBuf, + #[arg(long)] + output: PathBuf, +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum Purpose { + Query, + Document, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Record { + id: String, + purpose: Purpose, + text: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Input { + contract: String, + records: Vec, +} + +fn validate_input(input: &Input) -> Result<()> { + ensure!(input.contract == INPUT_CONTRACT, "input_contract_mismatch"); + ensure!( + (1..=30_000).contains(&input.records.len()), + "input_record_count_invalid" + ); + let mut seen = HashSet::new(); + for record in &input.records { + ensure!( + !record.id.trim().is_empty() && record.id.len() <= 512, + "record_id_invalid" + ); + ensure!(seen.insert(&record.id), "duplicate_record_id"); + ensure!( + !record.text.trim().is_empty() && record.text.len() <= 65_536, + "record_text_invalid" + ); + } + Ok(()) +} + +fn validate_vectors(vectors: &[Vec], expected: usize) -> Result<()> { + ensure!(vectors.len() == expected, "vector_count_mismatch"); + for vector in vectors { + ensure!( + vector.len() == 768 && vector.iter().all(|x| x.is_finite()), + "vector_shape_invalid" + ); + let norm = vector.iter().map(|x| f64::from(*x).powi(2)).sum::(); + ensure!((norm - 1.0).abs() < 0.001, "vector_not_normalized"); + } + Ok(()) +} + +fn private_directory(path: &Path) -> Result<()> { + ensure!(path.is_absolute(), "state_path_not_absolute"); + let metadata = fs::symlink_metadata(path)?; + ensure!( + metadata.is_dir() && !metadata.is_symlink(), + "state_not_directory" + ); + ensure!(fs::canonicalize(path)? == path, "state_path_not_canonical"); + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + ensure!( + metadata.uid() == unsafe { libc::geteuid() } && metadata.mode() & 0o077 == 0, + "state_directory_not_private" + ); + } + #[cfg(not(unix))] + bail!("diagnostic_requires_unix_private_directory_validation"); + Ok(()) +} + +fn validate_isolation(root: &Path, runtime: &SidecarRuntimeConfig) -> Result<()> { + private_directory(root)?; + let cache = root.join("cache"); + let ipc = root.join("ipc"); + private_directory(&cache)?; + private_directory(&ipc)?; + ensure!(runtime.cache_root == cache, "diagnostic_cache_not_isolated"); + ensure!( + !runtime.embedding.allow_cpu, + "diagnostic_cpu_fallback_forbidden" + ); + let gate = codestory_retrieval::qualification_gate_environment(); + ensure!( + gate.directory.as_deref() == Some(ipc.as_os_str()), + "diagnostic_ipc_not_isolated" + ); + let nonce = gate.nonce_string().context("diagnostic_nonce_missing")?; + ensure!( + (16..=64).contains(&nonce.len()) + && nonce + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'), + "diagnostic_nonce_invalid" + ); + Ok(()) +} + +fn validate_state(args: &Args, runtime: &SidecarRuntimeConfig) -> Result<()> { + validate_isolation(&args.state_root, runtime)?; + ensure!( + args.output.parent() == Some(args.state_root.as_path()), + "output_outside_state" + ); + match fs::symlink_metadata(&args.output) { + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(e.into()), + Ok(_) => bail!("output_already_exists"), + } +} + +fn publish_result(path: &Path, bytes: &[u8]) -> Result<()> { + let parent = path.parent().context("output_parent_missing")?; + private_directory(parent)?; + let mut temporary = tempfile::NamedTempFile::new_in(parent)?; + temporary.write_all(bytes)?; + temporary.as_file().sync_all()?; + // Atomic no-clobber publication, including a destination racing preflight. + temporary.persist_noclobber(path)?; + Ok(()) +} + +fn read_input(path: &Path, expected: &str) -> Result { + let file = fs::File::open(path)?; + ensure!(file.metadata()?.is_file(), "input_not_regular_file"); + let mut bytes = Vec::new(); + file.take(MAX_INPUT_BYTES + 1).read_to_end(&mut bytes)?; + ensure!(bytes.len() as u64 <= MAX_INPUT_BYTES, "input_too_large"); + ensure!( + format!("{:x}", Sha256::digest(&bytes)) == expected, + "input_digest_mismatch" + ); + let input = serde_json::from_slice(&bytes)?; + validate_input(&input)?; + Ok(input) +} + +fn engine_receipt(identity: &EmbeddingEngineIdentity) -> Result { + ensure!( + identity.accelerator_execution_verified + && identity.worker_alive + && identity.load_error.is_none() + && identity.embedded_model + && identity.policy == "accelerated", + "engine_execution_unverified" + ); + Ok(serde_json::to_value(identity)?) +} + +fn main() -> Result<()> { + if std::env::args().nth(1).as_deref() == Some("internal-embedding-server") { + ensure!( + build_provenance::SOURCE_DIRTY.trim() == "false", + "dirty_diagnostic_binary" + ); + let runtime = SidecarRuntimeConfig::local(); + let root = runtime.cache_root.parent().context("state_root_missing")?; + validate_isolation(root, &runtime)?; + return codestory_cli::run_native_embedding_server(); + } + let args = Args::parse(); + let input = read_input(&args.input, &args.input_sha256)?; + let runtime = SidecarRuntimeConfig::local(); + validate_state(&args, &runtime)?; + ensure!( + build_provenance::SOURCE_DIRTY.trim() == "false", + "dirty_diagnostic_binary" + ); + let mut executable = fs::File::open(std::env::current_exe()?)?; + let mut executable_digest = Sha256::new(); + std::io::copy(&mut executable, &mut executable_digest)?; + codestory_cli::install_native_embedding_client_transport()?; + let started = Instant::now(); + let mut residency = PerUserEmbeddingClient::for_runtime(&runtime)?.acquire_residency_lease()?; + let initial_engine = engine_receipt(residency.identity())?; + let client = ProductEmbeddingClient::new(&runtime); + let mut results = Vec::with_capacity(input.records.len()); + let mut ordinal = 0; + while ordinal < input.records.len() { + let purpose = input.records[ordinal].purpose; + let end = input.records[ordinal..] + .iter() + .take(BATCH_SIZE) + .take_while(|record| record.purpose == purpose) + .count() + + ordinal; + let texts = input.records[ordinal..end] + .iter() + .map(|r| r.text.clone()) + .collect::>(); + let batch_started = Instant::now(); + let timeout = Some(Duration::from_secs(60)); + let vectors = match purpose { + Purpose::Query => client.embed_queries_with_control(&texts, timeout, &|| false), + Purpose::Document => client.embed_documents_with_control(&texts, timeout, &|| false), + } + .with_context(|| format!("encode_failed_at_records_{ordinal}_{end}"))?; + validate_vectors(&vectors, texts.len())?; + let batch_ms = batch_started.elapsed().as_millis(); + for (record, vector) in input.records[ordinal..end].iter().zip(vectors) { + results.push(serde_json::json!({ + "id": record.id, "purpose": record.purpose, + "text_sha256": format!("{:x}", Sha256::digest(record.text.as_bytes())), + "vector": vector, + })); + } + ordinal = end; + eprintln!( + "encoded {ordinal}/{} records; batch_ms={batch_ms}", + input.records.len() + ); + } + let final_engine = engine_receipt(&residency.revalidate()?)?; + for key in [ + "server_instance_id", + "load_generation", + "model_digest", + "ggml_build_identity", + ] { + ensure!( + initial_engine[key] == final_engine[key], + "engine_identity_changed: {key}" + ); + } + let receipt = serde_json::json!({ + "contract": "codestory.embedding-diagnostic-output/v1", + "authority": "post_failure_diagnostic_only", + "packet_decision": "not_evaluated", + "input_sha256": args.input_sha256, + "source_commit": build_provenance::SOURCE_COMMIT.trim(), + "source_tree": build_provenance::SOURCE_TREE.trim(), + "build_profile": build_provenance::BUILD_PROFILE.trim(), + "rustc": build_provenance::RUSTC_VV.trim(), + "binary_sha256": format!("{:x}", executable_digest.finalize()), + "initial_engine": initial_engine, "final_engine": final_engine, + "whole_encoding_wall_ms": started.elapsed().as_millis(), + "records": results, + }); + // Recheck the owned destination before publication; failed encoding creates no result. + validate_state(&args, &runtime)?; + publish_result(&args.output, &serde_json::to_vec(&receipt)?)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn input(records: serde_json::Value) -> Input { + serde_json::from_value(serde_json::json!({ + "contract": INPUT_CONTRACT, + "records": records, + })) + .unwrap() + } + + #[test] + fn reject_invalid_input_before_encoder_activation() { + let valid = serde_json::json!({"id":"q0","purpose":"query","text":"find α"}); + validate_input(&input(serde_json::json!([valid]))).unwrap(); + for records in [ + serde_json::json!([]), + serde_json::json!([valid, valid]), + serde_json::json!([{"id":"","purpose":"query","text":"x"}]), + serde_json::json!([{"id":"q","purpose":"query","text":" "}]), + serde_json::json!([{"id":"d","purpose":"document","text":"x".repeat(65_537)}]), + ] { + assert!(validate_input(&input(records)).is_err()); + } + let mut wrong_contract = input(serde_json::json!([valid])); + wrong_contract.contract = "other".into(); + assert!(validate_input(&wrong_contract).is_err()); + } + + #[test] + fn reject_missing_extra_nonfinite_or_unnormalized_vectors() { + let mut unit = vec![0.0; 768]; + unit[0] = 1.0; + validate_vectors(&[unit.clone()], 1).unwrap(); + assert!(validate_vectors(&[], 1).is_err()); + assert!(validate_vectors(&[unit.clone(), unit.clone()], 1).is_err()); + assert!(validate_vectors(&[vec![1.0]], 1).is_err()); + assert!(validate_vectors(&[vec![0.0; 768]], 1).is_err()); + unit[0] = f32::NAN; + assert!(validate_vectors(&[unit], 1).is_err()); + } + + #[test] + fn unknown_fields_and_purposes_cannot_change_the_encoder_contract() { + for record in [ + serde_json::json!({"id":"d","purpose":"rerank","text":"x"}), + serde_json::json!({"id":"d","purpose":"document","text":"x","truncate":true}), + ] { + assert!(serde_json::from_value::(record).is_err()); + } + } + + #[test] + fn input_digest_binds_the_actual_encoder_text() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("input.json"); + let bytes = serde_json::to_vec(&serde_json::json!({ + "contract": INPUT_CONTRACT, + "records": [{"id":"d0","purpose":"document","text":"α\nβ\n"}], + })) + .unwrap(); + fs::write(&path, &bytes).unwrap(); + let digest = format!("{:x}", Sha256::digest(&bytes)); + assert_eq!( + read_input(&path, &digest).unwrap().records[0].text, + "α\nβ\n" + ); + fs::write(&path, b"{}").unwrap(); + assert!(read_input(&path, &digest).is_err()); + } + + #[cfg(unix)] + #[test] + fn state_must_be_private_and_canonical_not_a_symlink() { + use std::os::unix::fs::{PermissionsExt, symlink}; + let directory = tempfile::tempdir().unwrap(); + let canonical = fs::canonicalize(directory.path()).unwrap(); + fs::set_permissions(&canonical, fs::Permissions::from_mode(0o700)).unwrap(); + private_directory(&canonical).unwrap(); + fs::set_permissions(&canonical, fs::Permissions::from_mode(0o755)).unwrap(); + assert!(private_directory(&canonical).is_err()); + fs::set_permissions(&canonical, fs::Permissions::from_mode(0o700)).unwrap(); + let link = canonical.join("alias"); + symlink(&canonical, &link).unwrap(); + assert!(private_directory(&link).is_err()); + } + + #[cfg(unix)] + #[test] + fn concurrent_output_is_never_replaced() { + use std::os::unix::fs::PermissionsExt; + let directory = tempfile::tempdir().unwrap(); + let canonical = fs::canonicalize(directory.path()).unwrap(); + fs::set_permissions(&canonical, fs::Permissions::from_mode(0o700)).unwrap(); + let path = canonical.join("result.json"); + publish_result(&path, b"first").unwrap(); + assert!(publish_result(&path, b"second").is_err()); + assert_eq!(fs::read(&path).unwrap(), b"first"); + assert_eq!(fs::read_dir(&canonical).unwrap().count(), 1); + } +} diff --git a/crates/codestory-bench/src/bin/codestory_witness_seam.rs b/crates/codestory-bench/src/bin/codestory_witness_seam.rs new file mode 100644 index 000000000..5e3d248f5 --- /dev/null +++ b/crates/codestory-bench/src/bin/codestory_witness_seam.rs @@ -0,0 +1,943 @@ +//! Capture, replay and authenticate the frozen witness experiment. This hidden +//! binary never launches models or decides an evidence-quality gate. + +use anyhow::{Context, Result, ensure}; +use clap::{Parser, Subcommand}; +use codestory_contracts::compilation::PacketCompilationPublicationV1; +use codestory_retrieval::benchmark_support::{WitnessLexicalPin, pin_witness_lexical_sources}; +use codestory_runtime::benchmark_support::{ + WitnessSeamDescriptor, freeze_witness_descriptors, run_witness_seam, +}; +use codestory_store::CoreReadSession; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use sha2::{Digest, Sha256}; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +#[path = "codestory_proof_availability/build_provenance.rs"] +mod build_provenance; + +#[derive(Parser)] +#[command(about = "Replay frozen descriptors through header and addressed hydration")] +struct Args { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Build a core and isolated lexical shard in a new external directory. + Prepare { + #[arg(long)] + project: PathBuf, + #[arg(long)] + output_dir: PathBuf, + }, + /// Freeze the first sixteen existing lexical hits, without hydration selection. + Capture { + #[arg(long)] + prepared: PathBuf, + #[arg(long)] + prepared_sha256: String, + #[arg(long)] + case_id: String, + #[arg(long)] + phrasing_id: String, + #[arg(long)] + question: String, + #[arg(long)] + output: PathBuf, + }, + Replay { + #[arg(long)] + manifest: PathBuf, + #[arg(long)] + manifest_sha256: String, + /// A new external receipt file. An existing file is never overwritten. + #[arg(long)] + output: PathBuf, + }, + /// Recompute the deterministic receipt against the live pinned authorities. + ValidateReceipt { + #[arg(long)] + manifest: PathBuf, + #[arg(long)] + manifest_sha256: String, + #[arg(long)] + receipt: PathBuf, + #[arg(long)] + receipt_sha256: String, + }, + /// Post-failure diagnostic only: compare the native unquoted term cutoff. + /// This cannot mint a canonical witness capture or a qualification result. + CoverageDiagnostic { + #[arg(long)] + prepared: PathBuf, + #[arg(long)] + prepared_sha256: String, + #[arg(long)] + question: String, + #[arg(long)] + output: PathBuf, + }, +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct Manifest { + contract: String, + case_id: String, + phrasing_id: String, + project_root: PathBuf, + storage_path: PathBuf, + lexical_root: PathBuf, + lexical_input_hash: String, + publication: PacketCompilationPublicationV1, + descriptors: Vec, + #[serde(default)] + capture: Option, +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct Prepared { + contract: String, + project_root: PathBuf, + storage_path: PathBuf, + lexical_root: PathBuf, + lexical_input_hash: String, + lexical_coverage: codestory_retrieval::benchmark_support::LexicalCoverage, + publication: PacketCompilationPublicationV1, + core_pointer: codestory_contracts::core_publication::CorePublicationPointerV1, + build: serde_json::Value, +} + +fn read_manifest(path: &Path, expected: &str) -> Result { + let bytes = std::fs::read(path).context("read frozen manifest")?; + ensure!( + expected.len() == 64 && format!("{:x}", Sha256::digest(&bytes)) == expected, + "frozen manifest digest mismatch" + ); + let manifest: Manifest = serde_json::from_slice(&bytes)?; + ensure!( + manifest.contract == "codestory.witness-seam-input/v1" + && !manifest.case_id.is_empty() + && !manifest.phrasing_id.is_empty(), + "unsupported or unidentified witness experiment" + ); + ensure!( + manifest.project_root.is_absolute() && manifest.storage_path.is_absolute(), + "experiment paths must be absolute" + ); + Ok(manifest) +} + +fn file_digest(path: &Path) -> Result { + let mut file = File::open(path)?; + let mut digest = Sha256::new(); + let mut buffer = [0_u8; 64 * 1024]; + loop { + let count = file.read(&mut buffer)?; + if count == 0 { + break; + } + digest.update(&buffer[..count]); + } + Ok(format!("{:x}", digest.finalize())) +} + +fn write_receipt(path: &Path, bytes: &[u8]) -> Result<()> { + let mut file = OpenOptions::new().write(true).create_new(true).open(path)?; + file.write_all(bytes)?; + file.sync_all()?; + Ok(()) +} + +fn main() -> Result<()> { + let args = Args::parse(); + // A dirty binary can exercise tests, but cannot produce experiment evidence. + ensure!( + build_provenance::SOURCE_DIRTY.trim() == "false", + "witness replay requires a clean-source binary" + ); + match args.command { + Command::Prepare { + project, + output_dir, + } => prepare(&project, &output_dir), + Command::Capture { + prepared, + prepared_sha256, + case_id, + phrasing_id, + question, + output, + } => capture( + &prepared, + &prepared_sha256, + case_id, + phrasing_id, + &question, + &output, + ), + Command::Replay { + manifest, + manifest_sha256, + output, + } => replay(&manifest, &manifest_sha256, &output), + Command::ValidateReceipt { + manifest, + manifest_sha256, + receipt, + receipt_sha256, + } => { + validate_receipt(&manifest, &manifest_sha256, &receipt, &receipt_sha256)?; + println!( + "{}", + json!({"contract": "codestory.witness-receipt-validation/v1", + "manifest_sha256": manifest_sha256, "receipt_sha256": receipt_sha256, + "build": build_identity()?}) + ); + Ok(()) + } + Command::CoverageDiagnostic { + prepared, + prepared_sha256, + question, + output, + } => coverage_diagnostic(&prepared, &prepared_sha256, &question, &output), + } +} + +fn coverage_diagnostic( + prepared_path: &Path, + expected: &str, + question: &str, + output: &Path, +) -> Result<()> { + ensure!(!question.trim().is_empty(), "question is required"); + let bytes = std::fs::read(prepared_path)?; + ensure!( + format!("{:x}", Sha256::digest(&bytes)) == expected, + "preparation digest mismatch" + ); + let prepared: Prepared = serde_json::from_slice(&bytes)?; + ensure!( + prepared.contract == "codestory.witness-preparation/v1", + "unexpected preparation contract" + ); + let pin = CoreReadSession::pin(&prepared.storage_path)?; + ensure!( + pin.pointer() == &prepared.core_pointer, + "prepared core publication changed" + ); + let (control, candidate) = + codestory_retrieval::benchmark_support::witness_lexical_coverage_diagnostic( + &prepared.lexical_root, + &pin.identity().generation_id, + &prepared.lexical_input_hash, + question, + )?; + let compile = |hits: &[codestory_retrieval::CandidateHit]| -> Result { + let lexical = pin_witness_lexical_sources( + &prepared.lexical_root, + &pin.identity().generation_id, + &prepared.lexical_input_hash, + &hits + .iter() + .map(|hit| hit.file_path.clone()) + .collect::>(), + )?; + let descriptors = freeze_witness_descriptors(&pin, &lexical, &prepared.project_root, hits)?; + let pair = run_witness_seam( + &pin, + Some(&lexical), + &prepared.project_root, + &prepared.publication, + &descriptors, + )?; + Ok(json!({ + "hits": hits, "descriptors": descriptors, + "input": pair.addressed_input, "output": pair.addressed, + })) + }; + let receipt = json!({ + "contract": "codestory.lexical-coverage-diagnostic/v1", + "authority": "post_failure_diagnostic_only", + "packet_decision": "not_evaluated", + "cannot_replace_phase1a": true, + "question_sha256": format!("{:x}", Sha256::digest(question.as_bytes())), + "prepared_sha256": expected, + "prepared_path": prepared_path, + // A historical, immutable preparation is an input to this diagnostic. + // It is not relabelled as a preparation by the executing source build. + "preparation_build": prepared.build, + "execution_build": build_identity()?, + "control": compile(&control)?, "without_count_cutoff": compile(&candidate)?, + }); + write_receipt(output, &serde_json::to_vec_pretty(&receipt)?)?; + println!("{} {}", file_digest(output)?, output.display()); + Ok(()) +} + +fn prepare(project: &Path, output: &Path) -> Result<()> { + use codestory_contracts::workspace::SourceIndexPolicy; + use codestory_runtime::{ + RetrievalProcessDefaults, RetrievalRuntimeDefaults, RetrievalRuntimeOverrides, Runtime, + RuntimeProcessConfig, RuntimeRetrievalConfig, RuntimeRetrievalProfile, + }; + let project = project.canonicalize()?; + ensure!( + output.is_absolute() && !output.starts_with(&project), + "preparation must be external to the repository" + ); + std::fs::create_dir(output).context("reserve a new preparation directory")?; + let output = output.canonicalize()?; + let storage_path = output.join("codestory.db"); + let defaults = + RetrievalProcessDefaults::new(output.join("runtime"), RetrievalRuntimeDefaults::default()); + let retrieval = RuntimeRetrievalConfig::for_project_profile_with_process_defaults( + Some(&project), + 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(project.clone(), storage_path.clone()) + .map_err(|error| anyhow::anyhow!(error.message))?; + runtime + .index_service() + .run_indexing_blocking_without_runtime_refresh(codestory_contracts::api::IndexMode::Full) + .map_err(|error| anyhow::anyhow!(error.message))?; + drop(runtime); + let pin = CoreReadSession::pin(&storage_path)?; + let lexical_root = output.join("lexical"); + let (lexical_input_hash, lexical_coverage) = + codestory_retrieval::benchmark_support::prepare_witness_lexical_shard( + &project, + &pin, + &lexical_root, + )?; + let prepared = Prepared { + contract: "codestory.witness-preparation/v1".into(), + project_root: project.clone(), + storage_path, + lexical_root, + lexical_input_hash: lexical_input_hash.clone(), + lexical_coverage, + publication: PacketCompilationPublicationV1 { + project_id: codestory_workspace::project_identity_v3(&project).project_id, + core_generation_id: pin.identity().generation_id.clone(), + retrieval_generation: Some(lexical_input_hash), + }, + core_pointer: pin.pointer().clone(), + build: build_identity()?, + }; + let path = output.join("prepared.json"); + write_receipt(&path, &serde_json::to_vec_pretty(&prepared)?)?; + println!("{} {}", file_digest(&path)?, path.display()); + Ok(()) +} + +fn capture( + prepared_path: &Path, + expected: &str, + case_id: String, + phrasing_id: String, + question: &str, + output: &Path, +) -> Result<()> { + ensure!(!output.exists(), "frozen capture already exists"); + ensure!(!question.trim().is_empty(), "question is required"); + let bytes = std::fs::read(prepared_path)?; + ensure!( + format!("{:x}", Sha256::digest(&bytes)) == expected, + "preparation digest mismatch" + ); + let prepared: Prepared = serde_json::from_slice(&bytes)?; + ensure!( + prepared.contract == "codestory.witness-preparation/v1", + "unexpected preparation contract" + ); + ensure!( + prepared.build == build_identity()?, + "preparation belongs to another build" + ); + let pin = CoreReadSession::pin(&prepared.storage_path)?; + ensure!( + pin.pointer() == &prepared.core_pointer, + "prepared core publication changed" + ); + let layout = codestory_retrieval::SidecarLayout { + lexical_data_dir: prepared.lexical_root.clone(), + semantic_data_dir: PathBuf::new(), + scip_artifacts_root: PathBuf::new(), + state_file: PathBuf::new(), + }; + let hits = codestory_retrieval::LexicalClient::new(&layout).search( + &layout, + &pin.identity().generation_id, + &prepared.lexical_input_hash, + question, + 16, + )?; + let lexical = pin_witness_lexical_sources( + &prepared.lexical_root, + &pin.identity().generation_id, + &prepared.lexical_input_hash, + &hits + .iter() + .map(|hit| hit.file_path.clone()) + .collect::>(), + )?; + let descriptors = freeze_witness_descriptors(&pin, &lexical, &prepared.project_root, &hits)?; + let manifest = Manifest { + contract: "codestory.witness-seam-input/v1".into(), + case_id, + phrasing_id, + project_root: prepared.project_root, + storage_path: prepared.storage_path, + lexical_root: prepared.lexical_root, + lexical_input_hash: prepared.lexical_input_hash.clone(), + publication: prepared.publication, + descriptors, + capture: Some(json!({ + "question_sha256": format!("{:x}", Sha256::digest(question.as_bytes())), + "query_ordinal": 0, "prepared_sha256": expected, + "prepared_path": prepared_path, + "lexical_input_hash": prepared.lexical_input_hash, + "lexical_coverage": prepared.lexical_coverage, + "raw_hits_sha256": format!("{:x}", Sha256::digest(serde_json::to_vec(&hits)?)), + "candidate_count": hits.len(), "candidate_limit": 16, + "semantic": false, "graph": false, + "scores": hits.iter().map(|hit| hit.score).collect::>(), + "build_commit": build_provenance::SOURCE_COMMIT.trim(), + "binary_sha256": file_digest(&std::env::current_exe()?)?, + })), + }; + write_receipt(output, &serde_json::to_vec_pretty(&manifest)?)?; + println!("{} {}", file_digest(output)?, output.display()); + Ok(()) +} + +fn replay(manifest_path: &Path, expected: &str, output: &Path) -> Result<()> { + ensure!(!output.exists(), "receipt already exists"); + let receipt = compute_receipt(manifest_path, expected)?; + write_receipt(output, &serde_json::to_vec_pretty(&receipt)?)?; + println!("{} {}", file_digest(output)?, output.display()); + Ok(()) +} + +fn build_identity() -> Result { + Ok(json!({ + "source_commit": build_provenance::SOURCE_COMMIT.trim(), + "source_tree": build_provenance::SOURCE_TREE.trim(), + "profile": build_provenance::BUILD_PROFILE.trim(), + "rustc": build_provenance::RUSTC_VV.trim(), + "binary_sha256": file_digest(&std::env::current_exe()?)?, + })) +} + +fn compute_receipt(manifest_path: &Path, expected: &str) -> Result { + let manifest = read_manifest(manifest_path, expected)?; + let capture = manifest + .capture + .as_ref() + .context("capture authority missing")?; + let preparation = Path::new( + capture["prepared_path"] + .as_str() + .context("preparation path missing")?, + ); + let prepared_bytes = std::fs::read(preparation)?; + ensure!( + format!("{:x}", Sha256::digest(&prepared_bytes)) + == capture["prepared_sha256"] + .as_str() + .context("preparation digest missing")?, + "preparation digest changed" + ); + let prepared: Prepared = serde_json::from_slice(&prepared_bytes)?; + let build = build_identity()?; + ensure!( + prepared.contract == "codestory.witness-preparation/v1" + && prepared.build == build + && capture["build_commit"] == build["source_commit"] + && capture["binary_sha256"] == build["binary_sha256"], + "preparation, capture, and replay build identities differ" + ); + ensure!( + manifest.project_root == prepared.project_root + && manifest.storage_path == prepared.storage_path + && manifest.lexical_root == prepared.lexical_root + && manifest.lexical_input_hash == prepared.lexical_input_hash + && manifest.publication == prepared.publication + && capture["lexical_input_hash"] == prepared.lexical_input_hash, + "manifest differs from its preparation authority" + ); + let pin = CoreReadSession::pin(&manifest.storage_path)?; + ensure!( + pin.pointer() == &prepared.core_pointer, + "prepared core pointer changed" + ); + let lexical = manifest_lexical_pin(&manifest)?; + let pair = run_witness_seam( + &pin, + Some(&lexical), + &manifest.project_root, + &manifest.publication, + &manifest.descriptors, + )?; + Ok(json!({ + "contract": "codestory.witness-seam-receipt/v1", + "case_id": manifest.case_id, + "phrasing_id": manifest.phrasing_id, + "manifest_sha256": expected, + "descriptors_sha256": pair.descriptors_sha256, + "core_pointer": pin.pointer(), + "build": build, + "control": { + "input": pair.control_input, + "output": pair.control, + }, + "addressed": { + "input": pair.addressed_input, + "output": pair.addressed, + }, + "packet_decision": "not_evaluated", + })) +} + +fn validate_receipt( + manifest: &Path, + manifest_sha256: &str, + receipt: &Path, + receipt_sha256: &str, +) -> Result<()> { + let bytes = std::fs::read(receipt)?; + ensure!( + format!("{:x}", Sha256::digest(&bytes)) == receipt_sha256, + "receipt digest mismatch" + ); + let observed: serde_json::Value = serde_json::from_slice(&bytes)?; + ensure!( + observed == compute_receipt(manifest, manifest_sha256)?, + "receipt differs from deterministic hydration and compilation" + ); + Ok(()) +} + +fn manifest_lexical_pin(manifest: &Manifest) -> Result { + pin_witness_lexical_sources( + &manifest.lexical_root, + &manifest.publication.core_generation_id, + &manifest.lexical_input_hash, + &manifest + .descriptors + .iter() + .filter_map(|descriptor| { + descriptor + .path + .as_ref() + .map(|path| path.as_str().to_owned()) + }) + .collect::>(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn altered_or_unidentified_manifests_cannot_replay() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("input.json"); + let input = json!({ + "contract": "codestory.witness-seam-input/v1", "case_id": "case-a", + "phrasing_id": "original", "project_root": temp.path(), + "storage_path": temp.path().join("codestory.db"), + "lexical_root": temp.path().join("lexical"), "lexical_input_hash": "hash", + "publication": {"project_id": "p", "core_generation_id": "g", "retrieval_generation": null}, + "descriptors": [], + }); + let bytes = serde_json::to_vec(&input).unwrap(); + std::fs::write(&path, &bytes).unwrap(); + let digest = file_digest(&path).unwrap(); + assert!(read_manifest(&path, &digest).is_ok()); + std::fs::write(&path, [bytes.as_slice(), b" "].concat()).unwrap(); + assert!(read_manifest(&path, &digest).is_err()); + let mut invalid = input; + invalid["contract"] = json!("another-experiment"); + std::fs::write(&path, serde_json::to_vec(&invalid).unwrap()).unwrap(); + assert!(read_manifest(&path, &file_digest(&path).unwrap()).is_err()); + } + + #[test] + fn receipt_is_exclusive_and_preserves_the_first_result() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("receipt.json"); + write_receipt(&path, b"first").unwrap(); + assert!(write_receipt(&path, b"second").is_err()); + assert_eq!(std::fs::read(&path).unwrap(), b"first"); + } + + #[test] + fn isolated_capture_preserves_existing_lexical_addresses() { + let temp = tempfile::tempdir().unwrap(); + let project = temp.path().join("project"); + std::fs::create_dir(&project).unwrap(); + for ordinal in 0..16 { + std::fs::write( + project.join(format!("unit_{ordinal}.rs")), + format!( + "{}fn needle_{ordinal}() {{ let value = 1; }}\n", + "// unrelated header\n".repeat(80) + ), + ) + .unwrap(); + } + let output = temp.path().join("prepared"); + std::fs::write(project.join("image.png"), [0xff, 0xfe, 0xfd]).unwrap(); + prepare(&project, &output).unwrap(); + let preparation = output.join("prepared.json"); + let prepared: Prepared = + serde_json::from_slice(&std::fs::read(&preparation).unwrap()).unwrap(); + assert_eq!(prepared.lexical_coverage.unreadable_files, 1); + let captured = temp.path().join("capture.json"); + capture( + &preparation, + &file_digest(&preparation).unwrap(), + "synthetic".into(), + "original".into(), + "needle", + &captured, + ) + .unwrap(); + let manifest = read_manifest(&captured, &file_digest(&captured).unwrap()).unwrap(); + assert_eq!(manifest.descriptors.len(), 16); + assert!(manifest.descriptors.iter().all(|candidate| matches!( + candidate.anchor, + Some( + codestory_contracts::evidence_address::EvidenceAnchorV1::Match { .. } + | codestory_contracts::evidence_address::EvidenceAnchorV1::IndexedNode { .. } + ) + ))); + let pin = CoreReadSession::pin(&manifest.storage_path).unwrap(); + let lexical = manifest_lexical_pin(&manifest).unwrap(); + let pair = run_witness_seam( + &pin, + Some(&lexical), + &manifest.project_root, + &manifest.publication, + &manifest.descriptors, + ) + .unwrap(); + assert_eq!( + pair.control_input.admissions, + pair.addressed_input.admissions + ); + assert_eq!( + pair.control_input.sources.len(), + pair.addressed_input.sources.len() + ); + assert!( + pair.addressed_input + .sources + .iter() + .all(|source| source.source.contains("needle")) + ); + assert!(!output.join("runtime/models").exists()); + let diagnostic_path = temp.path().join("coverage-diagnostic.json"); + coverage_diagnostic( + &preparation, + &file_digest(&preparation).unwrap(), + "needle alpha beta gamma delta epsilon zeta eta theta", + &diagnostic_path, + ) + .unwrap(); + let diagnostic_bytes = std::fs::read(&diagnostic_path).unwrap(); + let diagnostic: serde_json::Value = serde_json::from_slice(&diagnostic_bytes).unwrap(); + assert_eq!(diagnostic["authority"], "post_failure_diagnostic_only"); + assert_eq!(diagnostic["packet_decision"], "not_evaluated"); + assert_eq!(diagnostic["cannot_replace_phase1a"], true); + assert_eq!(diagnostic["control"]["descriptors"], json!([])); + assert_eq!( + diagnostic["without_count_cutoff"]["descriptors"] + .as_array() + .unwrap() + .len(), + 16 + ); + assert!(read_manifest(&diagnostic_path, &file_digest(&diagnostic_path).unwrap()).is_err()); + assert!( + coverage_diagnostic( + &preparation, + &file_digest(&preparation).unwrap(), + "needle", + &diagnostic_path, + ) + .is_err() + ); + assert_eq!(std::fs::read(&diagnostic_path).unwrap(), diagnostic_bytes); + let refused = temp.path().join("refused-diagnostic.json"); + assert!(coverage_diagnostic(&preparation, &"0".repeat(64), "needle", &refused).is_err()); + assert!(!refused.exists()); + let manifest_digest = file_digest(&captured).unwrap(); + let receipt = temp.path().join("receipt.json"); + replay(&captured, &manifest_digest, &receipt).unwrap(); + validate_receipt( + &captured, + &manifest_digest, + &receipt, + &file_digest(&receipt).unwrap(), + ) + .unwrap(); + let original = compute_receipt(&captured, &manifest_digest).unwrap(); + // Rehashing altered evidence must not authenticate a different operation. + let mut mutations = Vec::new(); + let mut changed = original.clone(); + changed["addressed"]["output"]["support"] = json!([]); + mutations.push(changed); + let mut changed = original.clone(); + changed["addressed"]["input"]["sources"][0]["source"] = + json!("unexposed fabricated source"); + changed["addressed"]["output"]["support"] = json!([]); + mutations.push(changed); + let mut changed = original.clone(); + changed["addressed"]["input"]["sources"] + .as_array_mut() + .unwrap() + .remove(0); + changed["addressed"]["input"]["admission_gaps"] = json!([{ + "kind": "source_budget_exceeded", "stable_identity": original["addressed"]["input"]["admissions"][0]["stable_identity"], + "exact_selector_ordinal": null, + }]); + mutations.push(changed); + let mut changed = original.clone(); + changed["addressed"]["output"]["support"] = json!([{ + "kind": "symbol_location", "path": "absent.rs", "symbol": "invented", + }]); + mutations.push(changed); + let mut changed = original.clone(); + changed["addressed"]["output"]["continuation"] = json!(["x".repeat(17000)]); + mutations.push(changed); + for (index, changed) in mutations.into_iter().enumerate() { + let path = temp.path().join(format!("altered-{index}.json")); + write_receipt(&path, &serde_json::to_vec(&changed).unwrap()).unwrap(); + assert!( + validate_receipt( + &captured, + &manifest_digest, + &path, + &file_digest(&path).unwrap() + ) + .is_err() + ); + } + let input: serde_json::Value = + serde_json::from_slice(&std::fs::read(&captured).unwrap()).unwrap(); + for (index, pointer) in [ + "/publication/project_id", + "/publication/core_generation_id", + "/publication/retrieval_generation", + "/lexical_input_hash", + "/capture/lexical_input_hash", + "/capture/build_commit", + "/capture/binary_sha256", + "/capture/prepared_sha256", + ] + .iter() + .enumerate() + { + let mut changed = input.clone(); + *changed.pointer_mut(pointer).unwrap() = json!("wrong-authority"); + let path = temp.path().join(format!("authority-{index}.json")); + write_receipt(&path, &serde_json::to_vec(&changed).unwrap()).unwrap(); + assert!( + compute_receipt(&path, &file_digest(&path).unwrap()).is_err(), + "accepted {pointer}" + ); + } + for count in [0, 1, 15] { + let pair = run_witness_seam( + &pin, + Some(&lexical), + &manifest.project_root, + &manifest.publication, + &manifest.descriptors[..count], + ) + .expect("natural retrieval underfill is preserved in both arms"); + assert_eq!(pair.control_input.admissions.len(), count); + assert_eq!( + pair.control_input.admissions, + pair.addressed_input.admissions + ); + } + // A path result remains unaddressed rather than inventing a lexical match. + let unaddressed = temp.path().join("path-capture.json"); + capture( + &preparation, + &file_digest(&preparation).unwrap(), + "synthetic".into(), + "path".into(), + "unit_0.rs", + &unaddressed, + ) + .unwrap(); + let manifest = read_manifest(&unaddressed, &file_digest(&unaddressed).unwrap()).unwrap(); + assert!(manifest.descriptors.iter().any(|candidate| matches!( + candidate.anchor, + Some(codestory_contracts::evidence_address::EvidenceAnchorV1::PathOnly { .. }) + ))); + let pair = run_witness_seam( + &pin, + Some(&manifest_lexical_pin(&manifest).unwrap()), + &manifest.project_root, + &manifest.publication, + &manifest.descriptors, + ) + .expect("missing source precision is an explicit gap, not a failed experiment"); + assert!(!pair.addressed_input.admission_gaps.is_empty()); + assert_eq!( + pair.control_input.admission_gaps, + pair.addressed_input.admission_gaps + ); + } + + #[test] + fn lexical_source_without_a_parser_file_is_authenticated_by_its_shard() { + let temp = tempfile::tempdir().unwrap(); + let project = temp.path().join("project"); + std::fs::create_dir(&project).unwrap(); + std::fs::write(project.join("lib.rs"), "pub fn ordinary() {}\n").unwrap(); + std::fs::write( + project.join("guide.rst"), + format!("{}needle_document\n", "preamble\n".repeat(40)), + ) + .unwrap(); + let output = temp.path().join("prepared"); + prepare(&project, &output).unwrap(); + let preparation = output.join("prepared.json"); + let captured = temp.path().join("capture.json"); + capture( + &preparation, + &file_digest(&preparation).unwrap(), + "synthetic".into(), + "original".into(), + "needle_document", + &captured, + ) + .unwrap(); + let manifest = read_manifest(&captured, &file_digest(&captured).unwrap()).unwrap(); + assert_eq!(manifest.descriptors.len(), 1); + let pin = CoreReadSession::pin(&manifest.storage_path).unwrap(); + assert!( + pin.storage() + .get_file_by_path(&project.join("guide.rst")) + .unwrap() + .is_none() + ); + let lexical = manifest_lexical_pin(&manifest).unwrap(); + let pair = run_witness_seam( + &pin, + Some(&lexical), + &manifest.project_root, + &manifest.publication, + &manifest.descriptors, + ) + .unwrap(); + assert_eq!(pair.addressed_input.sources.len(), 1); + assert!( + pair.addressed_input.sources[0] + .source + .contains("needle_document") + ); + let manifest_digest = file_digest(&captured).unwrap(); + let mut false_completeness = compute_receipt(&captured, &manifest_digest).unwrap(); + let mut false_header = false_completeness.clone(); + false_header["addressed"] = false_header["control"].clone(); + assert_ne!( + false_header, false_completeness, + "the lexical control must expose a different window" + ); + let header_receipt = temp.path().join("false-header.json"); + write_receipt(&header_receipt, &serde_json::to_vec(&false_header).unwrap()).unwrap(); + assert!( + validate_receipt( + &captured, + &manifest_digest, + &header_receipt, + &file_digest(&header_receipt).unwrap() + ) + .is_err() + ); + assert_eq!( + false_completeness["addressed"]["input"]["sources"][0]["parser_completeness"], + "unknown" + ); + for arm in ["control", "addressed"] { + false_completeness[arm]["input"]["sources"][0]["parser_completeness"] = + json!("complete"); + } + let altered = temp.path().join("false-completeness.json"); + write_receipt(&altered, &serde_json::to_vec(&false_completeness).unwrap()).unwrap(); + assert!( + validate_receipt( + &captured, + &manifest_digest, + &altered, + &file_digest(&altered).unwrap() + ) + .is_err() + ); + let mut wrong = read_manifest(&captured, &file_digest(&captured).unwrap()).unwrap(); + wrong.lexical_input_hash = "0".repeat(64); + assert!( + manifest_lexical_pin(&wrong).is_err(), + "a different lexical publication cannot authorize source" + ); + std::fs::write(project.join("guide.rst"), "replaced\n").unwrap(); + assert!( + run_witness_seam( + &pin, + Some(&lexical), + &manifest.project_root, + &manifest.publication, + &manifest.descriptors + ) + .is_err() + ); + let shard = manifest + .lexical_root + .join("shards") + .join(&manifest.publication.core_generation_id) + .join("lexical-index.sqlite3"); + let mut permissions = std::fs::metadata(&shard).unwrap().permissions(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + permissions.set_mode(0o600); + } + #[cfg(not(unix))] + permissions.set_readonly(false); + std::fs::set_permissions(&shard, permissions).unwrap(); + std::fs::OpenOptions::new() + .write(true) + .open(shard) + .unwrap() + .set_len(16) + .unwrap(); + assert!( + manifest_lexical_pin(&manifest).is_err(), + "component truncation invalidates its warm seal" + ); + } +} diff --git a/crates/codestory-contracts/src/evidence_address.rs b/crates/codestory-contracts/src/evidence_address.rs new file mode 100644 index 000000000..4d1ec6894 --- /dev/null +++ b/crates/codestory-contracts/src/evidence_address.rs @@ -0,0 +1,212 @@ +//! Internal source addresses. These are not additions to the public packet API. +//! +//! Coordinates identify evidence; they do not assert a lexical match for a +//! symbol, graph endpoint, or file-only discovery. Runtime authenticates ranges +//! against the indexed content digest while holding the publication pin. + +use crate::packet_projection_v3::Sha256DigestV3Dto; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum EvidenceAddressError { + #[error("expected a nonempty zero-based half-open byte range")] + ByteRange, + #[error("expected a nonempty one-based inclusive line range")] + LineRange, + #[error("expected a normalized project-relative path")] + Path, + #[error("expected a nonempty stable identity")] + Identity, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct RawByteRange { + start: u64, + end: u64, +} + +/// Zero-based, half-open UTF-8 byte offsets. Runtime checks character boundaries. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "RawByteRange")] +pub struct ByteRangeV1 { + start: u64, + end: u64, +} + +impl ByteRangeV1 { + pub fn new(start: u64, end: u64) -> Result { + if start >= end { + return Err(EvidenceAddressError::ByteRange); + } + Ok(Self { start, end }) + } + + pub fn start(self) -> u64 { + self.start + } + + pub fn end(self) -> u64 { + self.end + } +} + +impl TryFrom for ByteRangeV1 { + type Error = EvidenceAddressError; + + fn try_from(value: RawByteRange) -> Result { + Self::new(value.start, value.end) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct RawLineRange { + start: u32, + end: u32, +} + +/// One-based, inclusive source line numbers. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "RawLineRange")] +pub struct LineRangeV1 { + start: u32, + end: u32, +} + +impl LineRangeV1 { + pub fn new(start: u32, end: u32) -> Result { + if start == 0 || start > end { + return Err(EvidenceAddressError::LineRange); + } + Ok(Self { start, end }) + } + + pub fn start(self) -> u32 { + self.start + } + + pub fn end(self) -> u32 { + self.end + } +} + +impl TryFrom for LineRangeV1 { + type Error = EvidenceAddressError; + + fn try_from(value: RawLineRange) -> Result { + Self::new(value.start, value.end) + } +} + +/// Full project-relative identity, with forward slashes on every host. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(try_from = "String", into = "String")] +pub struct ProjectRelativePath(String); + +impl ProjectRelativePath { + pub fn new(value: impl Into) -> Result { + let value = value.into(); + if value + .chars() + .any(|character| character.is_control() || character == '\\') + || (value + .as_bytes() + .first() + .is_some_and(u8::is_ascii_alphabetic) + && value.as_bytes().get(1) == Some(&b':')) + || value.split('/').any(|part| matches!(part, "" | "." | "..")) + { + return Err(EvidenceAddressError::Path); + } + Ok(Self(value)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl TryFrom for ProjectRelativePath { + type Error = EvidenceAddressError; + + fn try_from(value: String) -> Result { + Self::new(value) + } +} + +impl From for String { + fn from(value: ProjectRelativePath) -> Self { + value.0 + } +} + +// Distinct types prevent a relation identity from being resolved as a node. +macro_rules! stable_identity { + ($name:ident) => { + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] + #[serde(try_from = "String", into = "String")] + pub struct $name(String); + + impl $name { + pub fn new(value: impl Into) -> Result { + let value = value.into(); + if value.trim().is_empty() || value.chars().any(char::is_control) { + return Err(EvidenceAddressError::Identity); + } + Ok(Self(value)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + } + + impl TryFrom for $name { + type Error = EvidenceAddressError; + + fn try_from(value: String) -> Result { + Self::new(value) + } + } + + impl From<$name> for String { + fn from(value: $name) -> Self { + value.0 + } + } + }; +} + +stable_identity!(StableNodeId); +stable_identity!(StableRelationId); + +/// The digest covers the entire source file, not just the selected range. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SourceRangeV1 { + pub path: ProjectRelativePath, + pub byte_range: ByteRangeV1, + pub line_range: LineRangeV1, + pub content_digest: Sha256DigestV3Dto, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum EvidenceAnchorV1 { + Match { + byte_range: ByteRangeV1, + line_range: LineRangeV1, + }, + IndexedNode { + node_id: StableNodeId, + source_range: SourceRangeV1, + }, + RelationOccurrence { + relation_id: StableRelationId, + source_range: SourceRangeV1, + }, + PathOnly { + path: ProjectRelativePath, + }, +} diff --git a/crates/codestory-contracts/src/lib.rs b/crates/codestory-contracts/src/lib.rs index 2f6299fed..529f48633 100644 --- a/crates/codestory-contracts/src/lib.rs +++ b/crates/codestory-contracts/src/lib.rs @@ -18,6 +18,7 @@ pub mod compilation; pub mod config_registry; pub mod core_publication; pub mod events; +pub mod evidence_address; pub mod graph; pub mod grounding; pub mod installed_agent_timing; diff --git a/crates/codestory-contracts/tests/evidence_address.rs b/crates/codestory-contracts/tests/evidence_address.rs new file mode 100644 index 000000000..def5925df --- /dev/null +++ b/crates/codestory-contracts/tests/evidence_address.rs @@ -0,0 +1,69 @@ +use codestory_contracts::evidence_address::{ + ByteRangeV1, EvidenceAnchorV1, LineRangeV1, ProjectRelativePath, +}; +use serde_json::json; + +#[test] +fn evidence_addresses_reject_invalid_coordinates_and_path_authority() { + for (start, end) in [(0, 0), (5, 4)] { + assert!(ByteRangeV1::new(start, end).is_err()); + assert!( + serde_json::from_value::(json!({"start": start, "end": end})).is_err() + ); + } + for (start, end) in [(0, 1), (2, 1)] { + assert!(LineRangeV1::new(start, end).is_err()); + assert!( + serde_json::from_value::(json!({"start": start, "end": end})).is_err() + ); + } + for path in [ + "", + "/absolute", + "../outside", + "src/../outside", + "src//file", + "./file", + "C:/file", + "src\\file", + "src/\nfile", + ] { + assert!(ProjectRelativePath::new(path).is_err(), "{path:?}"); + assert!(serde_json::from_value::(json!(path)).is_err()); + } + let bytes = ByteRangeV1::new(0, 4).unwrap(); + assert_eq!((bytes.start(), bytes.end()), (0, 4)); + let lines = LineRangeV1::new(1, 1).unwrap(); + assert_eq!((lines.start(), lines.end()), (1, 1)); + assert_eq!( + ProjectRelativePath::new("src/日本語 file.rs") + .unwrap() + .as_str(), + "src/日本語 file.rs" + ); +} + +#[test] +fn nonlexical_anchors_never_gain_invented_match_coordinates() { + let range = json!({ + "path": "src/unit.rs", "byte_range": {"start": 4, "end": 30}, + "line_range": {"start": 2, "end": 3}, "content_digest": "a".repeat(64), + }); + for value in [ + json!({"kind": "match", "byte_range": {"start": 4, "end": 8}, "line_range": {"start": 2, "end": 2}}), + json!({"kind": "indexed_node", "node_id": "node:7", "source_range": range}), + json!({"kind": "relation_occurrence", "relation_id": "edge:9", "source_range": range}), + json!({"kind": "path_only", "path": "src/unit.rs"}), + ] { + let anchor: EvidenceAnchorV1 = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(anchor).unwrap(), value); + let mut forged = value.clone(); + forged["matched_line"] = json!(1); + assert!(serde_json::from_value::(forged).is_err()); + } + let mut malformed = json!({"kind": "indexed_node", "node_id": "", "source_range": range}); + assert!(serde_json::from_value::(malformed.clone()).is_err()); + malformed["node_id"] = json!("node:7"); + malformed["source_range"]["content_digest"] = json!("missing"); + assert!(serde_json::from_value::(malformed).is_err()); +} diff --git a/crates/codestory-retrieval/src/lexical_client.rs b/crates/codestory-retrieval/src/lexical_client.rs index 90d7ba211..79f069d9d 100644 --- a/crates/codestory-retrieval/src/lexical_client.rs +++ b/crates/codestory-retrieval/src/lexical_client.rs @@ -95,7 +95,7 @@ impl LexicalClient { } } -fn lexical_hit_to_candidate(hit: LexicalHit) -> Result { +pub(crate) fn lexical_hit_to_candidate(hit: LexicalHit) -> Result { use super::candidate::{CandidateHit, CandidateSource}; let mut candidate = CandidateHit::with_source( hit.path, diff --git a/crates/codestory-retrieval/src/lexical_index.rs b/crates/codestory-retrieval/src/lexical_index.rs index 69b89eef1..f3fa40be2 100644 --- a/crates/codestory-retrieval/src/lexical_index.rs +++ b/crates/codestory-retrieval/src/lexical_index.rs @@ -589,6 +589,48 @@ pub(crate) struct LexicalSourceInput { source_seals: Vec, } +/// The witness harness builds one immutable base, never a delta chain. Read +/// source hashes from that authenticated base, not from the live worktree or +/// the parser inventory (which intentionally omits many plain-text files). +#[cfg(feature = "benchmark-support")] +pub(crate) fn witness_source_hashes( + lexical_root: &Path, + generation: &str, + input_hash: &str, + paths: &[String], +) -> Result> { + anyhow::ensure!( + paths.len() <= 16, + "witness source request exceeds its frozen pool" + ); + let shard = shard_dir_for(lexical_root, generation); + let set = read_lexical_component_set(&shard, Some(generation), Some(input_hash))? + .context("witness lexical publication missing")?; + anyhow::ensure!( + set.deltas.is_empty(), + "witness preparation must be one immutable base" + ); + validate_lexical_component_set_files(&shard, &set)?; + let connection = open_read_only(&shard.join(&set.base.file_name))?; + let mut query = connection.prepare( + "SELECT content FROM lexical_documents WHERE document_key = ?1 AND source = 'lexical_source'", + )?; + let mut hashes = BTreeMap::new(); + for path in paths { + let content: Option = query + .query_row([format!("source\0{path}")], |row| row.get(0)) + .optional()?; + if let Some(content) = content { + hashes.insert( + path.clone(), + format!("{:x}", Sha256::digest(content.as_bytes())), + ); + } + } + validate_lexical_component_set_files(&shard, &set)?; + Ok(hashes) +} + #[derive(Clone)] pub(crate) struct PreparedLexicalInput { pub fingerprint: LexicalInputFingerprint, @@ -1669,7 +1711,7 @@ where query, limit, Arc::clone(&cancelled), - LexicalHitPayload::Full, + LexicalHitPayload::Full.into(), ); if result.is_err() && cancelled() { bail!("lexical search cancelled"); @@ -1694,7 +1736,7 @@ where query, limit, Arc::clone(&cancelled), - LexicalHitPayload::DescriptorOnly, + LexicalHitPayload::DescriptorOnly.into(), ); if result.is_err() && cancelled() { bail!("lexical descriptor search cancelled"); @@ -1708,7 +1750,7 @@ fn search_lexical_index_with_cancel_inner( query: &str, limit: usize, cancelled: Arc bool + Send + Sync>, - payload: LexicalHitPayload, + options: LexicalSearchOptions, ) -> Result> { if cancelled() { bail!("lexical search cancelled"); @@ -1730,7 +1772,7 @@ fn search_lexical_index_with_cancel_inner( query, limit, cancelled, - payload, + options, ); } let index_path = shard_dir.join(LEXICAL_INDEX_FILE); @@ -1757,7 +1799,7 @@ fn search_lexical_index_with_cancel_inner( document_count, &mut HashMap::new(), cancelled.as_ref(), - payload, + options, ) } @@ -1790,7 +1832,7 @@ pub(crate) fn search_lexical_index_batch_with_cancel( query, *limit, Arc::clone(&cancelled), - LexicalHitPayload::Full, + LexicalHitPayload::Full.into(), ) }) .collect(); @@ -1823,7 +1865,7 @@ pub(crate) fn search_lexical_index_batch_with_cancel( document_count, &mut token_frequencies, cancelled.as_ref(), - LexicalHitPayload::Full, + LexicalHitPayload::Full.into(), ) }) .collect() @@ -1835,7 +1877,7 @@ fn search_lexical_component_set( query: &str, limit: usize, cancelled: Arc bool + Send + Sync>, - payload: LexicalHitPayload, + options: LexicalSearchOptions, ) -> Result> { if cancelled() { bail!("lexical search cancelled"); @@ -1860,7 +1902,7 @@ fn search_lexical_component_set( component_limit, logical_count, Arc::clone(&cancelled), - payload, + options, )?; let mut hits = Vec::new(); for hit in base_hits { @@ -1877,7 +1919,7 @@ fn search_lexical_component_set( component_limit, logical_count, Arc::clone(&cancelled), - payload, + options, )?; for hit in delta_hits { let key = lexical_hit_document_key(&hit)?; @@ -1911,7 +1953,7 @@ fn search_lexical_component( limit: usize, logical_document_count: usize, cancelled: Arc bool + Send + Sync>, - payload: LexicalHitPayload, + options: LexicalSearchOptions, ) -> Result> { validate_component_descriptor_at(shard_dir, descriptor)?; let path = shard_dir.join(&descriptor.file_name); @@ -1929,7 +1971,7 @@ fn search_lexical_component( logical_document_count, &mut HashMap::new(), cancelled.as_ref(), - payload, + options, ) } @@ -1961,6 +2003,60 @@ fn lexical_hit_identity(hit: &LexicalHit) -> LexicalCandidateIdentity { ) } +/// Compare the incumbent and no-count-cutoff intervention through the same +/// component-aware reader. This is unavailable to product callers. +#[cfg(feature = "benchmark-support")] +pub(crate) fn witness_coverage_diagnostic( + lexical_root: &Path, + generation: &str, + input_hash: &str, + query: &str, +) -> Result<(Vec, Vec)> { + let shard = shard_dir_for(lexical_root, generation); + let control = search_lexical_index_with_cancel_inner( + &shard, + input_hash, + query, + 16, + Arc::new(|| false), + LexicalHitPayload::Full.into(), + )?; + let candidate = search_lexical_index_with_cancel_inner( + &shard, + input_hash, + query, + 16, + Arc::new(|| false), + LexicalSearchOptions { + payload: LexicalHitPayload::Full, + admission: LexicalAdmission::AnyMatchingTerm, + }, + )?; + Ok((control, candidate)) +} + +#[derive(Clone, Copy)] +struct LexicalSearchOptions { + payload: LexicalHitPayload, + admission: LexicalAdmission, +} + +impl From for LexicalSearchOptions { + fn from(payload: LexicalHitPayload) -> Self { + Self { + payload, + admission: LexicalAdmission::CurrentCoverage, + } + } +} + +#[derive(Clone, Copy)] +enum LexicalAdmission { + CurrentCoverage, + #[cfg(any(test, feature = "benchmark-support"))] + AnyMatchingTerm, +} + fn search_lexical_index_on_connection( connection: &Connection, query: &str, @@ -1968,8 +2064,9 @@ fn search_lexical_index_on_connection( document_count: usize, frequency_cache: &mut HashMap, cancelled: &(dyn Fn() -> bool + Send + Sync), - payload: LexicalHitPayload, + options: LexicalSearchOptions, ) -> Result> { + let LexicalSearchOptions { payload, admission } = options; if cancelled() { bail!("lexical search cancelled"); } @@ -2017,7 +2114,11 @@ fn search_lexical_index_on_connection( // Coverage is a count of distinct query terms. Rarity weights order // candidates within lexical lanes; an unmatched rare term must not veto // the documented two-of-three or forty-percent admission contracts. - let required_match_count = required_lexical_match_count(tokens.len()); + let required_match_count = match admission { + LexicalAdmission::CurrentCoverage => required_lexical_match_count(tokens.len()), + #[cfg(any(test, feature = "benchmark-support"))] + LexicalAdmission::AnyMatchingTerm => 1, + }; let exact_candidates = query_exact_candidates(connection, query, candidate_limit, payload)?; let path_candidates = query_fts_candidates( @@ -4767,7 +4868,7 @@ mod tests { 1, &mut HashMap::new(), &|| false, - LexicalHitPayload::DescriptorOnly, + LexicalHitPayload::DescriptorOnly.into(), ) .expect("descriptor path reads FTS membership and metadata only"); assert_eq!(descriptors.len(), 1); @@ -4781,7 +4882,7 @@ mod tests { 1, &mut HashMap::new(), &|| false, - LexicalHitPayload::Full, + LexicalHitPayload::Full.into(), ) .is_err(), "the hostile stored body must fail if a query tries to materialize it" @@ -5039,6 +5140,69 @@ mod tests { assert_eq!(required_lexical_match_count(12), 5); } + #[test] + fn coverage_diagnostic_changes_only_the_unquoted_term_cutoff() { + for identifier in ["frindle", "widget", "client", "cache"] { + let project = TempDir::new().expect("project"); + std::fs::write( + project.path().join("unit.rs"), + format!("fn {identifier}() {{}}\n"), + ) + .expect("literal source"); + let data = TempDir::new().expect("data"); + let shard = build(project.path(), data.path(), "cutoff-probe", "input"); + let connection = open_read_only(&shard.join(LEXICAL_INDEX_FILE)).expect("read pin"); + let query = format!("{identifier} alpha beta gamma delta epsilon zeta eta theta"); + let search = |query: &str, admission, limit, cancelled: bool| { + search_lexical_index_on_connection( + &connection, + query, + limit, + 1, + &mut HashMap::new(), + &move || cancelled, + LexicalSearchOptions { + payload: LexicalHitPayload::Full, + admission, + }, + ) + }; + assert!( + search(&query, LexicalAdmission::CurrentCoverage, 16, false) + .expect("current search") + .is_empty() + ); + let diagnostic = search(&query, LexicalAdmission::AnyMatchingTerm, 16, false) + .expect("diagnostic search"); + assert_eq!( + diagnostic.len(), + 1, + "a literal match survives the diagnostic cutoff" + ); + let exact = search(identifier, LexicalAdmission::CurrentCoverage, 16, false) + .expect("exact current search"); + assert_eq!(diagnostic[0].target, exact[0].target); + assert_eq!(diagnostic[0].start_line, exact[0].start_line); + assert_eq!(diagnostic[0].source_excerpt, exact[0].source_excerpt); + assert!( + search( + &format!("{query} `absent`"), + LexicalAdmission::AnyMatchingTerm, + 16, + false + ) + .expect("quoted selector remains mandatory") + .is_empty() + ); + assert!( + search(&query, LexicalAdmission::AnyMatchingTerm, 0, false) + .expect("zero limit") + .is_empty() + ); + assert!(search(&query, LexicalAdmission::AnyMatchingTerm, 16, true).is_err()); + } + } + #[test] fn whole_file_path_only_match_has_an_explicit_file_target() { let project = TempDir::new().expect("project"); diff --git a/crates/codestory-retrieval/src/lib.rs b/crates/codestory-retrieval/src/lib.rs index e5c81b15e..7111ebc56 100644 --- a/crates/codestory-retrieval/src/lib.rs +++ b/crates/codestory-retrieval/src/lib.rs @@ -63,9 +63,102 @@ pub mod benchmark_support { AttestedSemanticPoint, AttestedVectorPublication, EmbeddedVectorIndex, ExpectedVectorAnchor, SemanticPoint, VectorEvidenceContract, }; + pub use crate::lexical_index::LexicalCoverage; use anyhow::{Context, Result}; use std::path::{Path, PathBuf}; + pub struct WitnessLexicalPin { + generation: String, + input_hash: String, + source_hashes: std::collections::BTreeMap, + } + + impl WitnessLexicalPin { + pub fn generation(&self) -> &str { + &self.generation + } + pub fn input_hash(&self) -> &str { + &self.input_hash + } + pub fn source_hash(&self, path: &str) -> Option<&str> { + self.source_hashes.get(path).map(String::as_str) + } + } + + pub fn pin_witness_lexical_sources( + lexical_root: &Path, + generation: &str, + input_hash: &str, + paths: &[String], + ) -> Result { + Ok(WitnessLexicalPin { + generation: generation.into(), + input_hash: input_hash.into(), + source_hashes: crate::lexical_index::witness_source_hashes( + lexical_root, + generation, + input_hash, + paths, + )?, + }) + } + + /// Isolated post-failure intervention, never a product search policy. + /// Both arms use the same native ranking and differ only in the unquoted + /// term-count cutoff. Mandatory quoted terms retain their existing meaning. + pub fn witness_lexical_coverage_diagnostic( + lexical_root: &Path, + generation: &str, + input_hash: &str, + query: &str, + ) -> Result<(Vec, Vec)> { + let (control, candidate) = crate::lexical_index::witness_coverage_diagnostic( + lexical_root, + generation, + input_hash, + query, + )?; + Ok(( + control + .into_iter() + .map(crate::lexical_client::lexical_hit_to_candidate) + .collect::>()?, + candidate + .into_iter() + .map(crate::lexical_client::lexical_hit_to_candidate) + .collect::>()?, + )) + } + + /// Prepare the existing lexical implementation without semantic or graph + /// work. Only the frozen witness experiment consumes this isolated shard. + pub fn prepare_witness_lexical_shard( + project_root: &Path, + core: &codestory_store::CoreReadSession, + lexical_root: &Path, + ) -> Result<(String, LexicalCoverage)> { + let source = + crate::lexical_index::lexical_source_input(project_root, core.generation_path())?; + let expected = crate::lexical_index::prepare_lexical_input_for_store( + source, + project_root, + core.storage(), + )?; + let input_hash = expected.fingerprint.hash.clone(); + let coverage = expected.fingerprint.coverage.clone(); + crate::lexical_index::build_prepared_lexical_shard( + lexical_root, + &core.identity().generation_id, + &expected, + &input_hash, + None, + || expected.revalidate_source_seals(project_root, core.generation_path()), + )?; + // The witness experiment preserves the existing candidate universe and + // its omissions. This does not attest to installed-product readiness. + Ok((input_hash, coverage)) + } + /// One vector the bake-off publishes and later scores against. #[derive(Debug, Clone, PartialEq)] pub struct BenchmarkVector { diff --git a/crates/codestory-runtime/src/addressed_hydration.rs b/crates/codestory-runtime/src/addressed_hydration.rs new file mode 100644 index 000000000..2bbcbc5f7 --- /dev/null +++ b/crates/codestory-runtime/src/addressed_hydration.rs @@ -0,0 +1,140 @@ +//! Benchmark-only hydration. Callers supply source authenticated to their core +//! pin; this boundary rechecks the content and coordinate binding before slicing. +//! It cannot query, select identities, traverse relations, or inspect wording. + +use codestory_contracts::evidence_address::{ByteRangeV1, LineRangeV1, SourceRangeV1}; +use sha2::{Digest, Sha256}; +use std::fmt::Write; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddressedHydrationGap { + ContentChanged, + InvalidCoordinates, + PathMismatch, + SourceBudgetExceeded, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HydratedAddressedRange { + pub range: SourceRangeV1, + /// Verbatim source bytes, including the original line endings. + pub source: String, + /// Same line-numbered presentation as the current compiler's source input. + pub markdown: String, + pub truncated: bool, +} + +/// Select the smallest supplied syntax span containing the address. If it does +/// not fit, grow a complete-line window about the address, inside that span. +/// An oversized focus line is a typed gap, never a prefix or header substitute. +pub fn hydrate_addressed_range( + source: &str, + focus: &SourceRangeV1, + syntax: &[SourceRangeV1], + max_bytes: usize, +) -> Result { + let digest = format!("{:x}", Sha256::digest(source)); + let mut offsets = vec![0usize]; + for line in source.split_inclusive('\n') { + offsets.push(offsets.last().copied().unwrap_or(0) + line.len()); + } + validate_range(source, &offsets, &digest, focus)?; + let mut bounds = (0, offsets.len() - 1); + let mut best_bytes = u64::MAX; + for span in syntax { + if span.path != focus.path { + return Err(AddressedHydrationGap::PathMismatch); + } + validate_range(source, &offsets, &digest, span)?; + if span.byte_range.start() <= focus.byte_range.start() + && span.byte_range.end() >= focus.byte_range.end() + { + let size = span.byte_range.end() - span.byte_range.start(); + let candidate = ( + span.line_range.start() as usize - 1, + span.line_range.end() as usize, + ); + if (size, candidate) < (best_bytes, bounds) { + best_bytes = size; + bounds = candidate; + } + } + } + let centre = (focus.line_range.start() as usize - 1) + + (focus.line_range.end() - focus.line_range.start()) as usize / 2; + let line_cost = |index: usize| { + let text = source[offsets[index]..offsets[index + 1]].trim_end_matches(['\r', '\n']); + // Marker, line number, separator, newline. Fences cost eleven bytes. + 1 + (index + 1).to_string().len().max(5) + 3 + text.len() + 1 + }; + let mut start = centre; + let mut end = centre + 1; + let mut bytes = 11usize.saturating_add(line_cost(centre)); + if bytes > max_bytes { + return Err(AddressedHydrationGap::SourceBudgetExceeded); + } + loop { + let mut changed = false; + // Nearest lines first, with a stable left-before-right tie break. + if start > bounds.0 && bytes.saturating_add(line_cost(start - 1)) <= max_bytes { + start -= 1; + bytes += line_cost(start); + changed = true; + } + if end < bounds.1 && bytes.saturating_add(line_cost(end)) <= max_bytes { + bytes += line_cost(end); + end += 1; + changed = true; + } + if !changed { + break; + } + } + let mut markdown = String::with_capacity(bytes); + markdown.push_str("```text\n"); + for index in start..end { + let text = source[offsets[index]..offsets[index + 1]].trim_end_matches(['\r', '\n']); + let marker = if index == centre { '>' } else { ' ' }; + let _ = writeln!(markdown, "{marker}{:>5} | {text}", index + 1); + } + markdown.push_str("```"); + Ok(HydratedAddressedRange { + range: SourceRangeV1 { + path: focus.path.clone(), + byte_range: ByteRangeV1::new(offsets[start] as u64, offsets[end] as u64) + .map_err(|_| AddressedHydrationGap::InvalidCoordinates)?, + line_range: LineRangeV1::new(start as u32 + 1, end as u32) + .map_err(|_| AddressedHydrationGap::InvalidCoordinates)?, + content_digest: focus.content_digest.clone(), + }, + source: source[offsets[start]..offsets[end]].to_string(), + markdown, + truncated: (start, end) != bounds, + }) +} + +fn validate_range( + source: &str, + offsets: &[usize], + digest: &str, + range: &SourceRangeV1, +) -> Result<(), AddressedHydrationGap> { + if !range.content_digest.as_str().eq_ignore_ascii_case(digest) { + return Err(AddressedHydrationGap::ContentChanged); + } + let start = usize::try_from(range.byte_range.start()) + .map_err(|_| AddressedHydrationGap::InvalidCoordinates)?; + let end = usize::try_from(range.byte_range.end()) + .map_err(|_| AddressedHydrationGap::InvalidCoordinates)?; + if end > source.len() || !source.is_char_boundary(start) || !source.is_char_boundary(end) { + return Err(AddressedHydrationGap::InvalidCoordinates); + } + let first_line = offsets.partition_point(|offset| *offset <= start); + let last_line = offsets.partition_point(|offset| *offset < end); + if first_line != range.line_range.start() as usize + || last_line != range.line_range.end() as usize + { + return Err(AddressedHydrationGap::InvalidCoordinates); + } + Ok(()) +} diff --git a/crates/codestory-runtime/src/agent/packet_compiler.rs b/crates/codestory-runtime/src/agent/packet_compiler.rs index 12cb0b477..d0ec8adb4 100644 --- a/crates/codestory-runtime/src/agent/packet_compiler.rs +++ b/crates/codestory-runtime/src/agent/packet_compiler.rs @@ -36,7 +36,8 @@ use codestory_store::{FileInfo, Store}; use std::collections::{BTreeSet, HashMap}; use std::path::{Path, PathBuf}; -const COMPILER_SOURCE_TRUNCATION_SUFFIX: &str = "\n// ... source truncated by packet row cap\n```"; +pub(crate) const COMPILER_SOURCE_TRUNCATION_SUFFIX: &str = + "\n// ... source truncated by packet row cap\n```"; pub(crate) struct FrozenPacketCompilationV1 { pub(crate) product: RepositoryDerivedCompilationV1, @@ -273,7 +274,7 @@ fn hydrate_admitted_file_source( hydrated_source(admission, &path, None, &bounded.markdown) } -fn hydrated_source( +pub(crate) fn hydrated_source( admission: &codestory_contracts::compilation::PacketAdmissionReceiptV1, path: &str, symbol: Option, diff --git a/crates/codestory-runtime/src/lib.rs b/crates/codestory-runtime/src/lib.rs index a24eccc9e..1fced6a58 100644 --- a/crates/codestory-runtime/src/lib.rs +++ b/crates/codestory-runtime/src/lib.rs @@ -339,14 +339,24 @@ pub use path_resolution::resolve_project_file_path_from_root; mod process_config; pub use process_config::RuntimeProcessConfig; mod activation_retrieval; +#[cfg(feature = "benchmark-support")] +mod addressed_hydration; mod query_language; mod repository_identity; mod retrieval_boundary; mod search; mod search_runtime; #[cfg(feature = "benchmark-support")] +mod witness_seam; +#[cfg(feature = "benchmark-support")] pub mod benchmark_support { + pub use crate::addressed_hydration::{ + AddressedHydrationGap, HydratedAddressedRange, hydrate_addressed_range, + }; pub use crate::search::engine::{SearchEngine, SymbolIndexSession, SymbolIndexWriteStats}; + pub use crate::witness_seam::{ + WitnessSeamDescriptor, WitnessSeamPair, freeze_witness_descriptors, run_witness_seam, + }; } mod activation_status; diff --git a/crates/codestory-runtime/src/snippets.rs b/crates/codestory-runtime/src/snippets.rs index f0b844a99..1dbb1f3fc 100644 --- a/crates/codestory-runtime/src/snippets.rs +++ b/crates/codestory-runtime/src/snippets.rs @@ -108,7 +108,22 @@ pub(super) fn bounded_markdown_snippet_from_path( truncation_suffix: &str, ) -> io::Result { let file = std::fs::File::open(path)?; - let mut reader = io::BufReader::new(file); + bounded_markdown_snippet_from_reader( + io::BufReader::new(file), + focus_line, + context, + max_bytes, + truncation_suffix, + ) +} + +pub(crate) fn bounded_markdown_snippet_from_reader( + mut reader: R, + focus_line: u32, + context: usize, + max_bytes: usize, + truncation_suffix: &str, +) -> io::Result { let context = context.min(DIRECT_SNIPPET_CONTEXT_LINE_CAP); let focus = focus_line.max(1) as usize; let start = focus.saturating_sub(context).max(1); @@ -153,7 +168,26 @@ pub(super) fn bounded_markdown_snippet_range_from_path( truncation_suffix: &str, ) -> io::Result { let file = std::fs::File::open(path)?; - let mut reader = io::BufReader::new(file); + bounded_markdown_snippet_range_from_reader( + io::BufReader::new(file), + focus_line, + start_line, + end_line, + context, + max_bytes, + truncation_suffix, + ) +} + +pub(crate) fn bounded_markdown_snippet_range_from_reader( + mut reader: R, + focus_line: u32, + start_line: u32, + end_line: u32, + context: usize, + max_bytes: usize, + truncation_suffix: &str, +) -> io::Result { let context = context.min(DIRECT_SNIPPET_CONTEXT_LINE_CAP) as u32; let focus = focus_line.max(1); let start = start_line.saturating_sub(context).max(1); diff --git a/crates/codestory-runtime/src/witness_seam.rs b/crates/codestory-runtime/src/witness_seam.rs new file mode 100644 index 000000000..a170ddbbf --- /dev/null +++ b/crates/codestory-runtime/src/witness_seam.rs @@ -0,0 +1,533 @@ +//! Frozen-descriptor Phase 1A experiment. Both arms retain the same (at most +//! sixteen) admissions and use one authenticated source snapshot. Only the +//! hydration address differs; missing precision remains a typed gap. + +use crate::addressed_hydration::{AddressedHydrationGap, hydrate_addressed_range}; +use crate::agent::packet_compiler::{COMPILER_SOURCE_TRUNCATION_SUFFIX, hydrated_source}; +use crate::snippets::{ + bounded_markdown_snippet_from_reader, bounded_markdown_snippet_range_from_reader, +}; +use anyhow::{Context, Result, bail, ensure}; +use codestory_agent::evidence_compiler::{ + RepositoryDerivedCompilationV1, compile_repository_evidence, +}; +use codestory_contracts::api::SupportUnitDto; +use codestory_contracts::compilation::{ + AnswerSufficiencyV1, PACKET_COMPILATION_CONTRACT_VERSION_V1, PACKET_RETRIEVAL_SCORE_VERSION_V1, + PUBLIC_PACKET_SERIALIZED_MAX_BYTES, PacketAdmissionGapKindV1, PacketAdmissionGapV1, + PacketAdmissionOriginV1, PacketAdmissionReceiptV1, PacketCompilationInputV1, + PacketCompilationPublicationV1, PacketContinuationSelectorV1, PacketParserCompletenessV1, +}; +use codestory_contracts::evidence_address::{ + ByteRangeV1, EvidenceAnchorV1, LineRangeV1, ProjectRelativePath, SourceRangeV1, StableNodeId, +}; +use codestory_contracts::graph::{NodeId, NodeKind}; +use codestory_contracts::packet_projection_v3::Sha256DigestV3Dto; +use codestory_retrieval::benchmark_support::WitnessLexicalPin; +use codestory_store::CoreReadSession; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::{BTreeMap, BTreeSet}; +use std::io::Cursor; +use std::path::Path; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct WitnessSeamDescriptor { + pub admission: PacketAdmissionReceiptV1, + pub path: Option, + pub symbol: Option, + pub anchor: Option, + pub content_digest: Option, +} + +pub struct WitnessSeamPair { + pub descriptors_sha256: String, + pub control_input: PacketCompilationInputV1, + pub addressed_input: PacketCompilationInputV1, + pub control: WitnessSeamOutput, + pub addressed: WitnessSeamOutput, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct WitnessSeamOutput { + pub publication: PacketCompilationPublicationV1, + pub answer_sufficiency: AnswerSufficiencyV1, + pub support: Vec, + pub continuation: Vec, + pub gap: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum WitnessOutputGap { + SerializedPublicBudget, +} + +fn bounded_output( + compiled: RepositoryDerivedCompilationV1, + publication: &PacketCompilationPublicationV1, +) -> Result { + let mut output = WitnessSeamOutput { + publication: publication.clone(), + answer_sufficiency: AnswerSufficiencyV1::NotAsserted, + support: compiled.support, + continuation: compiled.continuation, + gap: None, + }; + if serde_json::to_vec(&output)?.len() > PUBLIC_PACKET_SERIALIZED_MAX_BYTES { + output.support.clear(); + output.continuation.clear(); + output.gap = Some(WitnessOutputGap::SerializedPublicBudget); + } + ensure!( + serde_json::to_vec(&output)?.len() <= PUBLIC_PACKET_SERIALIZED_MAX_BYTES, + "publication metadata exceeds the public packet budget" + ); + Ok(output) +} + +/// Preserve the existing lexical identities and order, including candidates +/// without a source address. Neither dropping nor padding may repair underfill. +pub fn freeze_witness_descriptors( + pin: &CoreReadSession, + lexical: &WitnessLexicalPin, + project_root: &Path, + hits: &[codestory_retrieval::CandidateHit], +) -> Result> { + use codestory_contracts::api::SearchTargetDto; + ensure!( + hits.len() <= 16, + "Phase 1A candidate universe exceeds sixteen" + ); + ensure!( + lexical.generation() == pin.identity().generation_id, + "lexical/core pin mismatch" + ); + let root = project_root.canonicalize()?; + let mut descriptors = Vec::new(); + for (ordinal, hit) in hits.iter().enumerate() { + ensure!( + hit.source == codestory_retrieval::CandidateSource::Lexical, + "Phase 1A permits only lexical retrieval" + ); + let hit_path = Path::new(&hit.file_path); + let relative = if hit_path.is_absolute() { + hit_path.strip_prefix(&root).ok() + } else { + Some(hit_path) + }; + let path = relative + .and_then(Path::to_str) + .and_then(|path| ProjectRelativePath::new(path).ok()); + let mut descriptor = WitnessSeamDescriptor { + admission: PacketAdmissionReceiptV1 { + packet_ordinal: ordinal as u32, + stable_identity: hit + .packet_stable_identity() + .context("hit lacks stable identity")?, + score_version: PACKET_RETRIEVAL_SCORE_VERSION_V1.into(), + reserved_source_bytes: 512, + origin: PacketAdmissionOriginV1::Retrieval, + }, + path: path.clone(), + symbol: hit + .qualified_name + .clone() + .or_else(|| hit.symbol_name.clone()), + anchor: path.clone().map(|path| EvidenceAnchorV1::PathOnly { path }), + content_digest: None, + }; + // A namespace may have only a diagnostic URI. It still occupies its + // original admission; the URI never becomes a source path. + let Some(path) = path else { + descriptors.push(descriptor); + continue; + }; + if hit.node_id.is_none() { + descriptor.admission.stable_identity = format!("path:{}", path.as_str()); + } + let file = core_file(pin, &root, &path)?; + let node = hit + .node_id + .as_ref() + .map(|id| -> Result<_> { + pin.storage() + .get_node(NodeId(id.parse()?))? + .context("retrieved node missing") + }) + .transpose()?; + if node + .as_ref() + .is_some_and(|node| node.kind == NodeKind::FILE || node.file_node_id.is_none()) + { + descriptors.push(descriptor); + continue; + } + let hash = source_hash(pin, Some(lexical), &path, file.as_ref().map(|file| file.id))?; + let Some(hash) = hash else { + descriptors.push(descriptor); + continue; + }; + let digest = + Sha256DigestV3Dto::new(hash).map_err(|_| anyhow::anyhow!("invalid source digest"))?; + descriptor.content_digest = Some(digest.clone()); + let source = authenticated_source(&root, &path, &digest)?; + if let Some(node) = node { + let file = file.context("indexed node file missing from core pin")?; + ensure!( + node.file_node_id == Some(NodeId(file.id)), + "retrieved node/file mismatch" + ); + if let (Some(start), Some(end)) = (node.start_line, node.end_line) + && let Some(source_range) = full_line_range(&source, &path, &digest, start, end) + { + descriptor.anchor = Some(EvidenceAnchorV1::IndexedNode { + node_id: StableNodeId::new(descriptor.admission.stable_identity.clone())?, + source_range, + }); + } + } else if let Some(SearchTargetDto::FileRange { + file_path, + start_byte, + end_byte, + }) = &hit.target + { + ensure!( + file_path == &hit.file_path, + "lexical match path differs from its candidate" + ); + let start = *start_byte as usize; + let end = *end_byte as usize; + ensure!( + start < end + && end <= source.len() + && source.is_char_boundary(start) + && source.is_char_boundary(end), + "invalid lexical match offsets" + ); + let first = source.as_bytes()[..start] + .iter() + .filter(|byte| **byte == b'\n') + .count() as u32 + + 1; + let last = source.as_bytes()[..end - 1] + .iter() + .filter(|byte| **byte == b'\n') + .count() as u32 + + 1; + ensure!( + hit.start_line == Some(first), + "lexical matched line differs from its offsets" + ); + descriptor.anchor = Some(EvidenceAnchorV1::Match { + byte_range: ByteRangeV1::new(*start_byte as u64, *end_byte as u64)?, + line_range: LineRangeV1::new(first, last)?, + }); + } + descriptors.push(descriptor); + } + Ok(descriptors) +} + +pub fn run_witness_seam( + pin: &CoreReadSession, + lexical: Option<&WitnessLexicalPin>, + project_root: &Path, + publication: &PacketCompilationPublicationV1, + descriptors: &[WitnessSeamDescriptor], +) -> Result { + ensure!( + publication.core_generation_id == pin.identity().generation_id + && lexical.is_none_or(|lexical| lexical.generation() == publication.core_generation_id) + && publication.retrieval_generation.as_deref() + == lexical.map(WitnessLexicalPin::input_hash), + "frozen descriptor publication differs from the core/lexical pin" + ); + ensure!( + publication.project_id == codestory_workspace::project_identity_v3(project_root).project_id, + "publication belongs to another logical project" + ); + ensure!( + descriptors.len() <= 16, + "Phase 1A candidate universe exceeds sixteen" + ); + let mut identities = BTreeSet::new(); + for (ordinal, descriptor) in descriptors.iter().enumerate() { + ensure!( + descriptor.admission.reserved_source_bytes == 512 + && descriptor.admission.packet_ordinal as usize == ordinal, + "Phase 1A charge or order changed" + ); + ensure!( + identities.insert(&descriptor.admission.stable_identity), + "duplicate frozen identity" + ); + } + let descriptors_sha256 = format!("{:x}", Sha256::digest(serde_json::to_vec(descriptors)?)); + let mut control_input = PacketCompilationInputV1 { + contract_version: PACKET_COMPILATION_CONTRACT_VERSION_V1, + publication: publication.clone(), + admissions: descriptors + .iter() + .map(|descriptor| descriptor.admission.clone()) + .collect(), + sources: Vec::new(), + relations: Vec::new(), + ambiguities: Vec::new(), + admission_gaps: Vec::new(), + }; + let mut addressed_input = control_input.clone(); + let root = project_root.canonicalize()?; + let mut snapshots = BTreeMap::::new(); + for descriptor in descriptors { + if descriptor.anchor.is_none() + || matches!(descriptor.anchor, Some(EvidenceAnchorV1::PathOnly { .. })) + { + for input in [&mut control_input, &mut addressed_input] { + record_gap( + input, + descriptor, + PacketAdmissionGapKindV1::SourceUnavailable, + ); + } + continue; + } + let path = descriptor + .path + .as_ref() + .context("address lacks source path")?; + let digest = descriptor + .content_digest + .as_ref() + .context("address lacks source digest")?; + let file = core_file(pin, &root, path)?; + let indexed_hash = source_hash(pin, lexical, path, file.as_ref().map(|file| file.id))? + .context("source has no pinned content binding")?; + ensure!( + indexed_hash == digest.as_str(), + "descriptor content is not the pinned content" + ); + let source = match snapshots.entry(path.clone()) { + std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(), + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(authenticated_source(&root, path, digest)?) + } + }; + let completeness = match &file { + Some(file) if file.indexed && file.complete => PacketParserCompletenessV1::Complete, + Some(file) if file.indexed => PacketParserCompletenessV1::Partial, + _ => PacketParserCompletenessV1::Unknown, + }; + let (focus, node_bounds) = match descriptor.anchor.as_ref().expect("checked above") { + EvidenceAnchorV1::Match { + byte_range, + line_range, + } => { + ensure!( + descriptor.admission.stable_identity == format!("path:{}", path.as_str()), + "file identity differs from frozen admission" + ); + ( + SourceRangeV1 { + path: path.clone(), + byte_range: *byte_range, + line_range: *line_range, + content_digest: digest.clone(), + }, + None, + ) + } + EvidenceAnchorV1::IndexedNode { + node_id, + source_range, + } => { + let file = file.as_ref().context("indexed node lacks its core file")?; + let id = node_id + .as_str() + .strip_prefix("node:") + .context("expected node identity")? + .parse()?; + let node = pin + .storage() + .get_node(NodeId(id))? + .context("node missing from pinned core")?; + ensure!( + node.file_node_id == Some(NodeId(file.id)) + && descriptor.admission.stable_identity == node_id.as_str(), + "node/file identity mismatch" + ); + let lines = LineRangeV1::new( + node.start_line.context("node start missing")?, + node.end_line.context("node end missing")?, + )?; + ensure!( + source_range.path == *path + && source_range.content_digest == *digest + && source_range.line_range == lines, + "node source address mismatch" + ); + (source_range.clone(), Some(lines)) + } + EvidenceAnchorV1::RelationOccurrence { .. } => { + bail!("graph evidence is disabled in Phase 1A") + } + EvidenceAnchorV1::PathOnly { .. } => unreachable!(), + }; + let mut syntax = Vec::new(); + if let Some(file) = &file { + for node in pin.storage().get_nodes_containing_source_lines( + NodeId(file.id), + focus.line_range.start(), + focus.line_range.end(), + )? { + if node.kind != NodeKind::FILE + && node.file_node_id == Some(NodeId(file.id)) + && let (Some(start), Some(end)) = (node.start_line, node.end_line) + && let Some(span) = full_line_range(source, path, digest, start, end) + { + syntax.push(span); + } + } + } + if node_bounds.is_some() { + syntax.push(focus.clone()); + } + let addressed = match hydrate_addressed_range(source, &focus, &syntax, 512) { + Ok(value) => Some(value.markdown), + Err(AddressedHydrationGap::SourceBudgetExceeded) => { + record_gap( + &mut addressed_input, + descriptor, + PacketAdmissionGapKindV1::SourceBudgetExceeded, + ); + None + } + Err(gap) => bail!("addressed hydration integrity failed: {gap:?}"), + }; + let control = if let Some(lines) = node_bounds { + bounded_markdown_snippet_range_from_reader( + Cursor::new(source.as_bytes()), + lines.start(), + lines.start(), + lines.end(), + 0, + 512, + COMPILER_SOURCE_TRUNCATION_SUFFIX, + )? + } else { + bounded_markdown_snippet_from_reader( + Cursor::new(source.as_bytes()), + 1, + 8, + 512, + COMPILER_SOURCE_TRUNCATION_SUFFIX, + )? + }; + for (input, markdown) in [ + (&mut control_input, Some(control.markdown)), + (&mut addressed_input, addressed), + ] { + if let Some(markdown) = markdown { + let mut hydrated = hydrated_source( + &descriptor.admission, + path.as_str(), + descriptor.symbol.clone(), + &markdown, + ) + .map_err(|gap| anyhow::anyhow!("compiler source conversion failed: {gap:?}"))?; + hydrated.parser_completeness = completeness; + input.sources.push(hydrated); + } + } + } + Ok(WitnessSeamPair { + descriptors_sha256, + control: bounded_output(compile_repository_evidence(&control_input), publication)?, + addressed: bounded_output(compile_repository_evidence(&addressed_input), publication)?, + control_input, + addressed_input, + }) +} + +fn record_gap( + input: &mut PacketCompilationInputV1, + descriptor: &WitnessSeamDescriptor, + kind: PacketAdmissionGapKindV1, +) { + input.admission_gaps.push(PacketAdmissionGapV1 { + kind, + stable_identity: Some(descriptor.admission.stable_identity.clone()), + exact_selector_ordinal: None, + }); +} + +fn core_file( + pin: &CoreReadSession, + root: &Path, + path: &ProjectRelativePath, +) -> Result> { + Ok(pin + .storage() + .get_file_by_path(&root.join(path.as_str()))? + .or(pin.storage().get_file_by_path(Path::new(path.as_str()))?)) +} + +fn source_hash( + pin: &CoreReadSession, + lexical: Option<&WitnessLexicalPin>, + path: &ProjectRelativePath, + file_id: Option, +) -> Result> { + let core = file_id + .map(|id| pin.storage().get_file_content_hash(id)) + .transpose()? + .flatten(); + let lexical = lexical.and_then(|pin| pin.source_hash(path.as_str())); + if let (Some(core), Some(lexical)) = (&core, lexical) { + ensure!(core == lexical, "core and lexical source bindings disagree"); + } + Ok(core.or_else(|| lexical.map(str::to_owned))) +} + +fn authenticated_source( + root: &Path, + path: &ProjectRelativePath, + digest: &Sha256DigestV3Dto, +) -> Result { + let full_path = root.join(path.as_str()).canonicalize()?; + ensure!( + full_path.starts_with(root), + "source escapes the selected repository" + ); + let source = std::fs::read_to_string(full_path)?; + ensure!( + format!("{:x}", Sha256::digest(&source)) == digest.as_str(), + "source changed since publication" + ); + Ok(source) +} + +fn full_line_range( + source: &str, + path: &ProjectRelativePath, + digest: &Sha256DigestV3Dto, + start: u32, + end: u32, +) -> Option { + let lines = LineRangeV1::new(start, end).ok()?; + let mut offsets = vec![0usize]; + for line in source.split_inclusive('\n') { + offsets.push(offsets.last()? + line.len()); + } + Some(SourceRangeV1 { + path: path.clone(), + content_digest: digest.clone(), + line_range: lines, + byte_range: ByteRangeV1::new( + *offsets.get(start as usize - 1)? as u64, + *offsets.get(end as usize)? as u64, + ) + .ok()?, + }) +} diff --git a/crates/codestory-runtime/tests/addressed_hydration.rs b/crates/codestory-runtime/tests/addressed_hydration.rs new file mode 100644 index 000000000..8ac866365 --- /dev/null +++ b/crates/codestory-runtime/tests/addressed_hydration.rs @@ -0,0 +1,84 @@ +#![cfg(feature = "benchmark-support")] + +use codestory_contracts::evidence_address::{ + ByteRangeV1, LineRangeV1, ProjectRelativePath, SourceRangeV1, +}; +use codestory_contracts::packet_projection_v3::Sha256DigestV3Dto; +use codestory_runtime::benchmark_support::hydrate_addressed_range; +use sha2::{Digest, Sha256}; + +fn source_range(source: &str, start: usize, end: usize) -> SourceRangeV1 { + SourceRangeV1 { + path: ProjectRelativePath::new("src/module.rs").unwrap(), + byte_range: ByteRangeV1::new(start as u64, end as u64).unwrap(), + line_range: LineRangeV1::new( + source.as_bytes()[..start] + .iter() + .filter(|byte| **byte == b'\n') + .count() as u32 + + 1, + source.as_bytes()[..end - 1] + .iter() + .filter(|byte| **byte == b'\n') + .count() as u32 + + 1, + ) + .unwrap(), + content_digest: Sha256DigestV3Dto::new(format!("{:x}", Sha256::digest(source))).unwrap(), + } +} + +#[test] +fn addressed_hydration_keeps_the_witness_after_a_long_header() { + let header = "// header only\n".repeat(100); + let function = "fn target() {\n next();\n}\n"; + let source = format!("{header}{function}"); + let start = source.find("next()").unwrap(); + let matched = source_range(&source, start, start + "next()".len()); + let syntax = source_range(&source, header.len(), source.len()); + let hydrated = hydrate_addressed_range(&source, &matched, &[syntax], 512).unwrap(); + assert_eq!(hydrated.source, function); + assert_eq!(hydrated.range.line_range.start(), 101); + assert_eq!(hydrated.range.line_range.end(), 103); + assert!(!hydrated.truncated); + assert!(!hydrated.markdown.contains("header only")); +} + +#[test] +fn hostile_source_windows_are_complete_content_bound_and_deterministic() { + let source = format!( + "fn enclosing() {{\r\n{} café();\r\n{}}}\r\n", + " alpha();\r\n".repeat(50), + " omega();\r\n".repeat(50) + ); + let start = source.find("café").unwrap(); + let matched = source_range(&source, start, start + "café".len()); + let syntax = source_range(&source, 0, source.len()); + let hydrated = hydrate_addressed_range(&source, &matched, &[syntax.clone()], 512).unwrap(); + assert!(hydrated.source.contains("café();\r\n")); + assert!(hydrated.source.ends_with("\r\n")); + assert!(hydrated.markdown.len() <= 512); + assert!(hydrated.truncated); + assert_eq!( + hydrated, + hydrate_addressed_range(&source, &matched, &[syntax], 512).unwrap() + ); + assert_eq!( + hydrated.source, + source + [hydrated.range.byte_range.start() as usize..hydrated.range.byte_range.end() as usize] + ); + + let mut changed = matched.clone(); + changed.content_digest = Sha256DigestV3Dto::new("0".repeat(64)).unwrap(); + assert!(hydrate_addressed_range(&source, &changed, &[], 512).is_err()); + changed = matched.clone(); + changed.line_range = LineRangeV1::new(1, 1).unwrap(); + assert!(hydrate_addressed_range(&source, &changed, &[], 512).is_err()); + changed = matched; + changed.byte_range = ByteRangeV1::new((start + 4) as u64, (start + 5) as u64).unwrap(); + assert!(hydrate_addressed_range(&source, &changed, &[], 512).is_err()); + + let giant = format!("{}\n", "é".repeat(600)); + assert!(hydrate_addressed_range(&giant, &source_range(&giant, 0, 2), &[], 512).is_err()); +} diff --git a/crates/codestory-runtime/tests/witness_seam.rs b/crates/codestory-runtime/tests/witness_seam.rs new file mode 100644 index 000000000..fe396e6eb --- /dev/null +++ b/crates/codestory-runtime/tests/witness_seam.rs @@ -0,0 +1,257 @@ +#![cfg(feature = "benchmark-support")] + +use codestory_contracts::compilation::{ + PacketAdmissionOriginV1, PacketAdmissionReceiptV1, PacketCompilationPublicationV1, +}; +use codestory_contracts::evidence_address::{ + ByteRangeV1, EvidenceAnchorV1, LineRangeV1, ProjectRelativePath, +}; +use codestory_contracts::graph::{Node, NodeId, NodeKind}; +use codestory_contracts::packet_projection_v3::Sha256DigestV3Dto; +use codestory_runtime::benchmark_support::{WitnessSeamDescriptor, run_witness_seam}; +use codestory_store::{ + CorePublicationLayout, CorePublishTransaction, CoreReadSession, FileInfo, FileRole, Store, +}; +use sha2::{Digest, Sha256}; + +#[test] +fn witness_seam_changes_only_hydration_under_one_core_pin() { + let temp = tempfile::tempdir().unwrap(); + let root_path = temp.path().canonicalize().unwrap(); + let root = root_path.as_path(); + std::fs::create_dir(root.join("src")).unwrap(); + let logical = root.join("codestory.db"); + let stage = CorePublicationLayout::from_storage_path(&logical) + .unwrap() + .create_staging_database_path() + .unwrap(); + let store = Store::open(&stage).unwrap(); + let mut descriptors = Vec::new(); + let mut hashes = Vec::new(); + for ordinal in 0..16 { + let relative = format!("src/unit_{ordinal}.rs"); + let source = format!( + "{}fn value_{ordinal}() {{\n work_{ordinal}();\n}}\n", + "// unrelated header\n".repeat(80) + ); + std::fs::write(root.join(&relative), &source).unwrap(); + let id = ordinal as i64 + 1; + store + .insert_node(&Node { + id: NodeId(id), + kind: NodeKind::FILE, + serialized_name: relative.clone(), + qualified_name: None, + canonical_id: None, + file_node_id: None, + start_line: None, + start_col: None, + end_line: None, + end_col: None, + }) + .unwrap(); + store + .insert_file(&FileInfo { + id, + path: root.join(&relative), + language: "rust".into(), + modification_time: 1, + indexed: true, + complete: ordinal % 2 == 0, + line_count: 83, + file_role: FileRole::default(), + }) + .unwrap(); + // The declaration span contains the match even without a token + // occurrence on the matched line. Hydration must use syntax extent. + store + .insert_node(&Node { + id: NodeId(id + 100), + kind: NodeKind::FUNCTION, + serialized_name: format!("value_{ordinal}"), + qualified_name: None, + canonical_id: None, + file_node_id: Some(NodeId(id)), + start_line: Some(81), + start_col: Some(1), + end_line: Some(83), + end_col: Some(2), + }) + .unwrap(); + let digest = format!("{:x}", Sha256::digest(&source)); + hashes.push((id, digest.clone())); + let start = source.find(&format!("work_{ordinal}")).unwrap(); + descriptors.push(WitnessSeamDescriptor { + admission: PacketAdmissionReceiptV1 { + packet_ordinal: ordinal, + stable_identity: format!("path:{relative}"), + score_version: "frozen-test/v1".into(), + reserved_source_bytes: 512, + origin: PacketAdmissionOriginV1::Retrieval, + }, + path: Some(ProjectRelativePath::new(relative).unwrap()), + symbol: None, + anchor: Some(EvidenceAnchorV1::Match { + byte_range: ByteRangeV1::new(start as u64, start as u64 + 4).unwrap(), + line_range: LineRangeV1::new(82, 82).unwrap(), + }), + content_digest: Some(Sha256DigestV3Dto::new(digest).unwrap()), + }); + } + drop(store); + let db = rusqlite::Connection::open(&stage).unwrap(); + for (id, hash) in hashes { + db.execute( + "UPDATE file SET content_hash=?1 WHERE id=?2", + rusqlite::params![hash, id], + ) + .unwrap(); + } + drop(db); + CorePublishTransaction::begin_from_stage(&logical, stage) + .unwrap() + .commit_rehydrate(&logical) + .unwrap(); + let pin = CoreReadSession::pin(&logical).unwrap(); + let publication = PacketCompilationPublicationV1 { + project_id: codestory_workspace::project_identity_v3(root).project_id, + core_generation_id: pin.identity().generation_id.clone(), + retrieval_generation: None, + }; + let pair = run_witness_seam(&pin, None, root, &publication, &descriptors).unwrap(); + assert_eq!( + pair.control_input.admissions, + pair.addressed_input.admissions + ); + assert_eq!(pair.control_input.admissions.len(), 16); + assert_eq!( + pair.control_input.publication, + pair.addressed_input.publication + ); + assert_eq!(pair.control_input.sources.len(), 16); + assert_eq!(pair.addressed_input.sources.len(), 16); + assert_eq!( + pair.addressed_input + .sources + .iter() + .filter(|source| source.parser_completeness + == codestory_contracts::compilation::PacketParserCompletenessV1::Partial) + .count(), + 8, + "partial parsing does not discard verified source or assert complete coverage" + ); + assert!( + pair.control_input + .sources + .iter() + .all(|source| source.start_line == 1 && source.end_line == 9) + ); + assert!(pair.control_input.sources.iter().all(|source| { + source.source.contains("unrelated header") && !source.source.contains("work_") + })); + assert!( + pair.addressed_input + .sources + .iter() + .all(|source| source.source.contains("work_")) + ); + assert!( + pair.addressed_input + .sources + .iter() + .all(|source| source.source.contains("fn value_") + && !source.source.contains("unrelated header")) + ); + assert_eq!(pair.control.support.len(), 16); + assert_eq!(pair.addressed.support.len(), 16); + assert!( + pair.addressed_input + .sources + .iter() + .all(|source| source.source.len() <= 512) + ); + + for count in [0, 1, 15] { + assert_eq!( + run_witness_seam(&pin, None, root, &publication, &descriptors[..count]) + .unwrap() + .addressed_input + .admissions + .len(), + count + ); + } + let mut wrong_publication = publication.clone(); + wrong_publication.project_id = "another-project".into(); + assert!(run_witness_seam(&pin, None, root, &wrong_publication, &descriptors).is_err()); + wrong_publication = publication.clone(); + wrong_publication.core_generation_id.push_str("-other"); + assert!(run_witness_seam(&pin, None, root, &wrong_publication, &descriptors).is_err()); + let mut changed_charge = descriptors.clone(); + changed_charge[0].admission.reserved_source_bytes = 513; + assert!(run_witness_seam(&pin, None, root, &publication, &changed_charge).is_err()); + let mut changed_order = descriptors.clone(); + changed_order.swap(0, 1); + assert!(run_witness_seam(&pin, None, root, &publication, &changed_order).is_err()); + let mut invalid = descriptors.clone(); + invalid[0].anchor = Some(EvidenceAnchorV1::PathOnly { + path: invalid[0].path.clone().unwrap(), + }); + let gaps = run_witness_seam(&pin, None, root, &publication, &invalid).unwrap(); + assert_eq!( + gaps.control_input.admission_gaps, + gaps.addressed_input.admission_gaps + ); + assert_eq!(gaps.addressed_input.sources.len(), 15); + invalid[0].path = None; + invalid[0].anchor = None; + invalid[0].content_digest = None; + let missing = run_witness_seam(&pin, None, root, &publication, &invalid).unwrap(); + assert_eq!( + missing.control_input.admission_gaps, + missing.addressed_input.admission_gaps + ); + assert_eq!(missing.addressed_input.admissions.len(), 16); + let mut oversized = descriptors.clone(); + for descriptor in &mut oversized { + descriptor + .admission + .stable_identity + .push_str(&"/long-path".repeat(200)); + descriptor.path = None; + descriptor.anchor = None; + descriptor.content_digest = None; + } + let bounded = run_witness_seam(&pin, None, root, &publication, &oversized).unwrap(); + for output in [bounded.control, bounded.addressed] { + let value = serde_json::to_value(&output).unwrap(); + assert_eq!(value["gap"], "serialized_public_budget"); + assert!(output.support.is_empty()); + assert!(output.continuation.is_empty()); + assert!(serde_json::to_vec(&output).unwrap().len() <= 16 * 1024); + } + let mut missing_node = descriptors.clone(); + missing_node[0].admission.stable_identity = "node:999999".into(); + let EvidenceAnchorV1::Match { + byte_range, + line_range, + } = missing_node[0].anchor.clone().unwrap() + else { + panic!("expected lexical anchor") + }; + missing_node[0].anchor = Some(EvidenceAnchorV1::IndexedNode { + node_id: codestory_contracts::evidence_address::StableNodeId::new("node:999999").unwrap(), + source_range: codestory_contracts::evidence_address::SourceRangeV1 { + path: missing_node[0].path.clone().unwrap(), + byte_range, + line_range, + content_digest: missing_node[0].content_digest.clone().unwrap(), + }, + }); + assert!(run_witness_seam(&pin, None, root, &publication, &missing_node).is_err()); + std::fs::write(root.join("src/unit_0.rs"), "changed\n").unwrap(); + assert!( + run_witness_seam(&pin, None, root, &publication, &descriptors).is_err(), + "source replacement invalidates both arms" + ); +} diff --git a/crates/codestory-store/src/storage_impl/mod.rs b/crates/codestory-store/src/storage_impl/mod.rs index 96d439fc0..b556aa672 100644 --- a/crates/codestory-store/src/storage_impl/mod.rs +++ b/crates/codestory-store/src/storage_impl/mod.rs @@ -13370,6 +13370,31 @@ impl Storage { Ok(node_kinds.into_values().collect()) } + /// Indexed declaration extents containing a source range, independent of + /// token occurrences on those lines. Used for addressed source hydration. + pub fn get_nodes_containing_source_lines( + &self, + file_id: NodeId, + start_line: u32, + end_line: u32, + ) -> Result, StorageError> { + if start_line == 0 || end_line < start_line { + return Ok(Vec::new()); + } + let mut statement = self.conn.prepare( + "SELECT id, kind, serialized_name, qualified_name, canonical_id, file_node_id, start_line, start_col, end_line, end_col + FROM node WHERE file_node_id = ?1 AND start_line > 0 + AND start_line <= ?2 AND end_line >= ?3 + ORDER BY end_line - start_line, start_line, id", + )?; + let mut rows = statement.query(params![file_id.0, start_line, end_line])?; + let mut nodes = Vec::new(); + while let Some(row) = rows.next()? { + nodes.push(Self::node_from_row(row)?); + } + Ok(nodes) + } + pub fn get_nodes_for_file_line( &self, path: &str, diff --git a/docs/contributors/testing-matrix.md b/docs/contributors/testing-matrix.md index aba89bc6e..c6dc89445 100644 --- a/docs/contributors/testing-matrix.md +++ b/docs/contributors/testing-matrix.md @@ -83,6 +83,63 @@ It authenticates the run ledger against the original receipt and reads only preserved transcript copies. The checksummed erratum withdraws affected comparisons without replacing the original receipt or its decision. +The hidden source-address experiment uses these focused checks, run serially: + +```sh +cargo test --locked -p codestory-contracts --test evidence_address +cargo test --locked -p codestory-runtime --features benchmark-support --test addressed_hydration --test witness_seam +cargo test --locked -p codestory-bench --bin codestory-witness-seam +node --test scripts/tests/witness-seam-evidence.test.mjs +``` + +`codestory-witness-seam` is a benchmark binary, not a public CLI or MCP route. +Its `prepare`, `capture`, and `replay` commands preserve the existing lexical +order and compare two hydration addresses under the same core and lexical pins. +Replay requires a clean-source binary and an authenticated frozen manifest. +Both arms keep the same actual candidates, up to sixteen, with 512-byte source +charges and the unchanged pure compiler. Empty or underfilled retrieval stays +in the experiment. Missing source addresses retain their admissions with typed +gaps, never header substitutes. Lexical-only files authenticate against their +sealed source documents; partial parsing does not erase authenticated source or +grant complete structural authority. Coverage omissions are recorded; +preparation does not certify full installed-product readiness. + +Keep questions, independent annotations, descriptors, and receipts outside the +repository. `scripts/codestory-witness-seam-evaluate.mjs` authenticates those +inputs and requires every case and phrasing before producing an aggregate. +Supply its explicit `--binary` with the frozen replay binary. Capture and replay +must each have a successful, exact invocation and authenticated stdout, stderr, +and output artifact. The native `validate-receipt` operation recomputes hydration +and compilation against the prepared core/lexical and build identities; matching +JSON hashes alone do not authenticate evidence. The complete public output, +including publication and continuation metadata, fits 16 KiB or returns a typed +`serialized_public_budget` gap. Detailed compiler inputs remain outside that +public payload. +Passing synthetic contracts does not pass the evidence-quality gate. Pool, +graph, semantic, public-routing, and release changes remain gated separately. + +After a failed witness experiment, the hidden `coverage-diagnostic` command can +compare the existing lexical cutoff with admitting any matching query term. +Both arms retain the native ranker, mandatory quoted terms, sixteen candidates, +512-byte hydration, and pure compiler. It reads a checksummed historical +preparation, records preparation and execution builds separately, and produces +only `codestory.lexical-coverage-diagnostic/v1` output. It cannot mint a canonical +capture, change the original decision, or authorize product routing. The +intervention exists only with `benchmark-support`; ordinary searches retain +their current cutoff. Run the lexical-index tests with that feature and the +witness binary tests when changing this diagnostic. + +The Cargo example `codestory_embedding_diagnostic` measures the existing native +encoder on checksummed external inputs. It is not a shipped command and emits +no packet or qualification decision. It requires a clean build with the pinned +embedded model, a private state root containing `cache/` and `ipc/`, and matching +cache and qualification-namespace environment settings. It retains the product +query prefix, document encoder, accelerator policy, and token limit. Any rejected +input aborts the result; it never truncates text or substitutes CPU execution. +Run `cargo test --locked -p codestory-bench --example codestory_embedding_diagnostic` +and an isolated native canary before using it for post-failure retrieval research. +Keep its vectors, inputs, and research conclusions outside the repository. + Run the relevant focused commands while implementing. A typical Rust lane is: ```sh diff --git a/scripts/codestory-witness-seam-evaluate.mjs b/scripts/codestory-witness-seam-evaluate.mjs new file mode 100644 index 000000000..42257e514 --- /dev/null +++ b/scripts/codestory-witness-seam-evaluate.mjs @@ -0,0 +1,163 @@ +import assert from "node:assert/strict"; +import { readFile, realpath, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { parseArgs } from "node:util"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; +import { authenticateRange, phase1AGate, scoreWitnessArm, sha256, verifyPairedInputs } from "./lib/witness-seam-evidence.mjs"; + +async function boundJson(file, digest) { + const bytes = await readFile(file); + assert.equal(sha256(bytes), digest, `artifact digest mismatch: ${file}`); + return JSON.parse(bytes); +} + +export async function verifyRequiredOperation(operation, command, artifact, digest) { + assert.ok(operation, "required operation record missing"); + assert.equal(operation.exit_code, 0, "required operation failed"); + assert.equal(operation.error, null, "required operation raised an error"); + assert.deepEqual(operation.command, command, "required operation command differs"); + assert.ok(Number.isFinite(operation.wall_ms) && operation.wall_ms >= 0, "operation timing missing"); + const stdout = await readFile(operation.stdout_path); + const stderr = await readFile(operation.stderr_path); + assert.equal(sha256(stdout), operation.stdout_sha256, "operation stdout changed"); + assert.equal(sha256(stderr), operation.stderr_sha256, "operation stderr changed"); + assert.equal(stdout.toString().trim(), `${digest} ${artifact}`, "operation returned the wrong artifact"); + assert.equal(sha256(await readFile(artifact)), digest, "operation artifact changed"); +} + +/** Evaluate every frozen case or emit no aggregate. No candidate selection. */ +export async function evaluateWitnessRun({ questions, annotations, runs, binary }) { + assert.ok(binary && path.isAbsolute(binary), "an explicit trusted replay binary is required"); + const binaryDigest = sha256(await readFile(binary)); + assert.equal(runs.binary_sha256, binaryDigest, "run binary differs from trusted binary"); + assert.equal(questions.authority, "visible_development_only"); + assert.equal(questions.cases.length, 24); + assert.equal(questions.repositories.length, 12); + assert.equal(runs.rows.length, questions.cases.length * 3); + const identities = new Set(); + const results = []; + let build; + for (const row of runs.rows) { + const key = `${row.case_id}/${row.phrasing_id}`; + assert.ok(!identities.has(key), "duplicate case/phrasing"); + identities.add(key); + const task = questions.cases.find((value) => value.case_id === row.case_id); + assert.ok(task, "unexpected evidence task"); + const phrasing = ["original", "paraphrase_1", "paraphrase_2"].indexOf(row.phrasing_id); + assert.ok(phrasing >= 0, "unexpected phrasing"); + const question = [task.question, ...task.paraphrases][phrasing]; + assert.equal(row.exit_code, 0, "required witness replay failed"); + const manifest = await boundJson(row.manifest_path, row.manifest_sha256); + const receipt = await boundJson(row.receipt_path, row.receipt_sha256); + assert.equal(manifest.case_id, row.case_id); + assert.equal(manifest.phrasing_id, row.phrasing_id); + assert.equal(manifest.capture.question_sha256, sha256(question)); + assert.equal(manifest.capture.query_ordinal, 0); + assert.equal(manifest.capture.candidate_limit, 16); + assert.equal(manifest.capture.candidate_count, manifest.descriptors.length); + assert.equal(manifest.capture.semantic, false); + assert.equal(manifest.capture.graph, false); + await verifyRequiredOperation(row.capture, [binary, "capture", "--prepared", manifest.capture.prepared_path, + "--prepared-sha256", manifest.capture.prepared_sha256, "--case-id", row.case_id, + "--phrasing-id", row.phrasing_id, "--question", question, "--output", row.manifest_path], + row.manifest_path, row.manifest_sha256); + await verifyRequiredOperation(row.replay, [binary, "replay", "--manifest", row.manifest_path, + "--manifest-sha256", row.manifest_sha256, "--output", row.receipt_path], row.receipt_path, row.receipt_sha256); + assert.equal(receipt.manifest_sha256, row.manifest_sha256); + assert.equal(receipt.descriptors_sha256, sha256(JSON.stringify(manifest.descriptors))); + build ??= receipt.build; + assert.deepEqual(receipt.build, build, "mixed replay binaries"); + assert.equal(manifest.capture.binary_sha256, build.binary_sha256, "capture and replay binaries differ"); + assert.equal(build.binary_sha256, binaryDigest, "receipt binary differs from trusted binary"); + assert.equal(manifest.capture.build_commit, build.source_commit, "capture source differs from replay"); + assert.equal(manifest.capture.lexical_input_hash, manifest.lexical_input_hash, "capture lexical authority differs"); + assert.equal(manifest.publication.retrieval_generation, manifest.lexical_input_hash, "retrieval publication differs"); + // The owning native path replays the complete transformation, including + // typed gaps, parser completeness, symbol membership and public capping. + // Do not independently reimplement hydration or the compiler in this scorer. + const validation = spawnSync(binary, ["validate-receipt", "--manifest", row.manifest_path, + "--manifest-sha256", row.manifest_sha256, "--receipt", row.receipt_path, + "--receipt-sha256", row.receipt_sha256], { encoding: "utf8", timeout: 60000, maxBuffer: 1024 * 1024 }); + assert.equal(validation.status, 0, `native receipt validation failed: ${validation.stderr}`); + assert.equal(validation.error, undefined, "native receipt validation raised an error"); + assert.deepEqual(JSON.parse(validation.stdout), { contract: "codestory.witness-receipt-validation/v1", + manifest_sha256: row.manifest_sha256, receipt_sha256: row.receipt_sha256, build }); + verifyPairedInputs(receipt, manifest); + const repository = questions.repositories.find((value) => value.id === task.repository_id); + assert.ok(repository, "missing repository binding"); + const root = await realpath(repository.local_root); + assert.equal(await realpath(manifest.project_root), root); + const annotation = annotations.cases.find((value) => value.case_id === row.case_id); + assert.ok(annotation, "missing reconciled annotation"); + const annotationRanges = annotation.acceptable_sets.flatMap((set) => [ + ...set.required_source_atoms.map((atom) => atom.source_range), ...set.allowed_support_ranges, + ...(set.required_relation_atoms ?? []).flatMap((atom) => [atom.occurrence, atom.from.source_range, atom.to.source_range]), + ]); + const paths = new Set([...manifest.descriptors.filter((value) => value.content_digest).map((value) => value.path), ...annotationRanges.map((range) => range.path)]); + const sources = new Map(); + for (const relative of paths) { + const absolute = await realpath(path.join(root, relative)); + assert.ok(absolute.startsWith(root + path.sep), "source escapes repository"); + sources.set(relative, await readFile(absolute)); + } + for (const range of annotationRanges) authenticateRange(range, sources); + for (const descriptor of manifest.descriptors) { + if (descriptor.content_digest) { + assert.equal(sha256(sources.get(descriptor.path)), descriptor.content_digest, "descriptor source changed"); + } + if (descriptor.anchor?.kind === "match") { + authenticateRange({ ...descriptor.anchor, path: descriptor.path, content_digest: descriptor.content_digest }, sources); + } else if (descriptor.anchor?.kind === "indexed_node") { + authenticateRange(descriptor.anchor.source_range, sources); + } + } + for (const arm of [receipt.control, receipt.addressed]) { + arm.input.sources.forEach((source) => { + const descriptor = manifest.descriptors.find((value) => value.admission.stable_identity === source.stable_identity); + assert.equal(source.path, descriptor.path, "hydration changed candidate path"); + }); + assert.deepEqual(arm.output.publication, manifest.publication); + assert.equal(arm.output.answer_sufficiency, "not_asserted"); + for (const output of arm.output.support.filter((value) => value.kind === "source_range")) { + assert.ok(arm.input.sources.some((source) => source.path === output.path && source.start_line === output.start_line + && source.end_line === output.end_line && source.source === output.snippet), "public source differs from hydrated input"); + } + } + results.push({ case_id: row.case_id, phrasing_id: row.phrasing_id, + control: scoreWitnessArm(receipt.control.output, annotation, sources, { headerControl: true }), + addressed: scoreWitnessArm(receipt.addressed.output, annotation, sources) }); + } + assert.equal(sha256(await readFile(binary)), binaryDigest, "trusted binary changed during validation"); + return { ...phase1AGate(results, questions.cases.map((value) => value.case_id)), + source_address_validity: 1, build, rows: results }; +} + +async function main() { + const { values } = parseArgs({ options: Object.fromEntries([ + "questions", "questions-sha256", "annotations", "annotations-sha256", "runs", "runs-sha256", "output", "binary", + ].map((name) => [name, { type: "string" }])) }); + for (const name of ["questions", "questions-sha256", "annotations", "annotations-sha256", "runs", "runs-sha256", "output", "binary"]) + assert.ok(values[name], `missing --${name}`); + const inputs = { questions: values["questions-sha256"], annotations: values["annotations-sha256"], runs: values["runs-sha256"] }; + let report; + try { + const questions = await boundJson(values.questions, inputs.questions); + const annotations = await boundJson(values.annotations, inputs.annotations); + assert.equal(annotations.questions_sha256, inputs.questions); + const runs = await boundJson(values.runs, inputs.runs); + assert.equal(runs.questions_sha256, inputs.questions); + assert.equal(runs.annotations_sha256, inputs.annotations); + report = { experiment_status: "valid", ...await evaluateWitnessRun({ questions, annotations, runs, binary: values.binary }), inputs }; + } catch (error) { + report = { contract: "codestory.witness-seam-evaluation/v1", experiment_status: "invalid", + phase1a: "blocked", packet_decision: "not_evaluated", inputs, error: error.message }; + } + const bytes = JSON.stringify(report, null, 2) + "\n"; + await writeFile(values.output, bytes, { flag: "wx" }); + console.log(`${sha256(bytes)} ${values.output}`); + if (report.experiment_status !== "valid" || report.phase1a !== "pass") process.exitCode = 1; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) + main().catch((error) => { console.error(error.message); process.exitCode = 1; }); diff --git a/scripts/lib/witness-seam-evidence.mjs b/scripts/lib/witness-seam-evidence.mjs new file mode 100644 index 000000000..7bccda3a4 --- /dev/null +++ b/scripts/lib/witness-seam-evidence.mjs @@ -0,0 +1,187 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; + +export const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex"); +const mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length; + +function merged(ranges) { + const result = []; + for (const [start, end] of ranges.toSorted((a, b) => a[0] - b[0] || a[1] - b[1])) { + const last = result.at(-1); + if (last && start <= last[1]) last[1] = Math.max(last[1], end); + else result.push([start, end]); + } + return result; +} + +function overlap(range, intervals) { + return merged(intervals).reduce((sum, [start, end]) => + sum + Math.max(0, Math.min(range[1], end) - Math.max(range[0], start)), 0); +} + +function lines(bytes) { + const offsets = [0]; + for (let i = 0; i < bytes.length; i++) if (bytes[i] === 10) offsets.push(i + 1); + if (offsets.at(-1) !== bytes.length) offsets.push(bytes.length); + return offsets; +} + +export function authenticateRange(range, sources) { + assert.ok(range.path && !range.path.startsWith("/") && !range.path.includes("\\") + && !range.path.split("/").some((part) => !part || part === "." || part === ".."), "invalid source path"); + const bytes = sources.get(range.path); + assert.ok(Buffer.isBuffer(bytes), `missing source ${range.path}`); + assert.equal(sha256(bytes), range.content_digest, "source digest mismatch"); + const { start, end } = range.byte_range; + assert.ok(Number.isSafeInteger(start) && Number.isSafeInteger(end) + && start >= 0 && end > start && end <= bytes.length, "invalid byte range"); + for (const offset of [start, end]) assert.ok(offset === bytes.length || (bytes[offset] & 0xc0) !== 0x80, "split UTF-8 range"); + const offsets = lines(bytes); + assert.equal(offsets.filter((offset) => offset <= start).length, range.line_range.start, "start line mismatch"); + assert.equal(offsets.filter((offset) => offset < end).length, range.line_range.end, "end line mismatch"); + return [start, end]; +} + +function witnessedBytes(row, sources, allowTruncatedLine) { + const bytes = sources.get(row.path); + assert.ok(bytes, "packet source file missing"); + const offsets = lines(bytes); + const presented = [...row.snippet.matchAll(/^[ >]\s*(\d+) \| (.*)$/gm)]; + assert.ok(presented.length, "source row has no authenticated lines"); + assert.equal(Number(presented[0][1]), row.start_line); + assert.equal(Number(presented.at(-1)[1]), row.end_line); + const ranges = []; + for (const [ordinal, match] of presented.entries()) { + const line = Number(match[1]); + assert.equal(line, row.start_line + ordinal, "non-contiguous source presentation"); + assert.ok(line > 0 && line < offsets.length, "source line out of bounds"); + const full = bytes.subarray(offsets[line - 1], offsets[line]); + const text = Buffer.from(full.toString("utf8").replace(/[\r\n]+$/, "")); + const shown = Buffer.from(match[2]); + assert.ok(shown.length <= text.length && shown.equals(text.subarray(0, shown.length)), "fabricated source text"); + if (shown.length < text.length) { + assert.ok(allowTruncatedLine && ordinal === presented.length - 1 + && row.snippet.includes("\n// ... source truncated by packet row cap\n"), "unmarked partial source line"); + } + ranges.push([offsets[line - 1], offsets[line - 1] + (shown.length === text.length ? full.length : shown.length)]); + } + return ranges; +} + +/** Evidence annotations are external inputs. Nothing here runs in the product. */ +export function scoreWitnessArm(arm, annotation, sources, { headerControl = false } = {}) { + assert.ok(annotation.acceptable_sets.length > 0, "no acceptable evidence sets"); + const allowed = new Map(); + for (const set of annotation.acceptable_sets) { + assert.ok(set.required_source_atoms.length > 0, "empty acceptable source set"); + for (const atom of set.required_source_atoms) authenticateRange(atom.source_range, sources); + for (const range of set.allowed_support_ranges) { + const intervals = allowed.get(range.path) ?? []; + intervals.push(authenticateRange(range, sources)); + allowed.set(range.path, intervals); + } + } + const witnesses = new Map(); + let exposed = 0, relevant = 0; + assert.ok(arm.support.length <= 16, "public row limit exceeded"); + assert.ok(Buffer.byteLength(JSON.stringify(arm)) <= 16 * 1024, "serialized public budget exceeded"); + if (arm.gap != null) { + assert.equal(arm.gap, "serialized_public_budget", "unknown public gap"); + assert.deepEqual(arm.support, []); + assert.deepEqual(arm.continuation, []); + } + for (const row of arm.support) { + assert.ok(["source_range", "symbol_location"].includes(row.kind), "Phase 1A cannot emit relation or claim rows"); + if (row.kind !== "source_range") continue; + const ranges = witnessedBytes(row, sources, headerControl); + const existing = witnesses.get(row.path) ?? []; + witnesses.set(row.path, existing.concat(ranges)); + for (const range of ranges) { + exposed += range[1] - range[0]; + relevant += overlap(range, allowed.get(row.path) ?? []); + } + } + const alternatives = annotation.acceptable_sets.map((set) => { + const covered = set.required_source_atoms.filter(({ source_range: range }) => + overlap([range.byte_range.start, range.byte_range.end], witnesses.get(range.path) ?? []) + === range.byte_range.end - range.byte_range.start).length; + return { set_id: set.set_id, recall: covered / set.required_source_atoms.length, + complete_source_set: covered === set.required_source_atoms.length }; + }); + const best = alternatives.toSorted((a, b) => b.recall - a.recall || a.set_id.localeCompare(b.set_id))[0]; + return { ...best, exposed_source_bytes: exposed, relevant_source_bytes: relevant, + relevant_byte_precision: exposed ? relevant / exposed : 0, + irrelevant_byte_ratio: exposed ? 1 - relevant / exposed : 1 }; +} + +export function verifyPairedInputs(receipt, manifest) { + assert.equal(receipt.contract, "codestory.witness-seam-receipt/v1"); + assert.equal(receipt.case_id, manifest.case_id); + assert.equal(receipt.phrasing_id, manifest.phrasing_id); + const control = receipt.control.input, addressed = receipt.addressed.input; + for (const input of [control, addressed]) { + assert.deepEqual(input.publication, manifest.publication); + assert.deepEqual(input.admissions, manifest.descriptors.map((value) => value.admission)); + assert.ok(input.admissions.length <= 16); + assert.deepEqual(input.relations, []); + assert.deepEqual(input.ambiguities, []); + const sources = new Map(input.sources.map((source) => [source.stable_identity, source])); + const gaps = new Map(input.admission_gaps.map((gap) => [gap.stable_identity, gap])); + assert.equal(sources.size, input.sources.length, "duplicate hydrated identity"); + assert.equal(gaps.size, input.admission_gaps.length, "duplicate source gap"); + assert.equal(sources.size + gaps.size, input.admissions.length, "every admission needs source or a gap"); + const orderedSources = []; + input.admissions.forEach((admission, ordinal) => { + assert.equal(admission.packet_ordinal, ordinal); + assert.equal(admission.reserved_source_bytes, 512); + const source = sources.get(admission.stable_identity), gap = gaps.get(admission.stable_identity); + assert.ok(Boolean(source) !== Boolean(gap), "source and gap must be exclusive"); + const anchor = manifest.descriptors[ordinal].anchor; + const unaddressed = !anchor || anchor.kind === "path_only"; + if (source) { + assert.ok(!unaddressed, "unaddressed candidate fabricated source"); + assert.ok(Buffer.byteLength(source.source) <= 512); + orderedSources.push(source); + } else { + assert.equal(gap.kind, unaddressed ? "source_unavailable" : "source_budget_exceeded"); + } + }); + assert.deepEqual(input.sources, orderedSources, "hydration changed candidate ordering"); + } + assert.equal(receipt.core_pointer.active.generation_id, manifest.publication.core_generation_id); + for (const source of control.sources) { + const other = addressed.sources.find((value) => value.stable_identity === source.stable_identity); + if (other) { + assert.equal(other.path, source.path); + assert.equal(other.parser_completeness, source.parser_completeness); + } + } +} + +export function phase1AGate(records, expectedCases) { + const caseIds = [...new Set(records.map((row) => row.case_id))].sort(); + assert.deepEqual(caseIds, [...expectedCases].sort(), "missing or unexpected evidence case"); + const cases = caseIds.map((case_id) => { + const rows = records.filter((row) => row.case_id === case_id); + assert.deepEqual(rows.map((row) => row.phrasing_id).sort(), ["original", "paraphrase_1", "paraphrase_2"]); + return { case_id, + control_recall: mean(rows.map((row) => row.control.recall)), + addressed_recall: mean(rows.map((row) => row.addressed.recall)), + control_irrelevant: mean(rows.map((row) => row.control.irrelevant_byte_ratio)), + addressed_irrelevant: mean(rows.map((row) => row.addressed.irrelevant_byte_ratio)) }; + }); + const controlRecall = mean(cases.map((row) => row.control_recall)); + const recall = mean(cases.map((row) => row.addressed_recall)); + const controlIrrelevant = mean(cases.map((row) => row.control_irrelevant)); + const irrelevant = mean(cases.map((row) => row.addressed_irrelevant)); + const gates = { + required_source_recall: recall >= 0.75, + recall_improvement: controlRecall >= 0.75 || recall - controlRecall >= 0.20, + irrelevant_bytes_reduction: irrelevant <= controlIrrelevant * 0.8, + }; + return { contract: "codestory.witness-seam-evaluation/v1", authority: "visible_development_only", + phase1a: Object.values(gates).every(Boolean) ? "pass" : "fail", gates, cases, + mean_control_recall: controlRecall, mean_addressed_recall: recall, + mean_control_irrelevant_byte_ratio: controlIrrelevant, mean_addressed_irrelevant_byte_ratio: irrelevant, + aggregation_unit: "case_mean_across_three_phrasings", packet_decision: "not_evaluated" }; +} diff --git a/scripts/tests/witness-seam-evidence.test.mjs b/scripts/tests/witness-seam-evidence.test.mjs new file mode 100644 index 000000000..b8c7d8383 --- /dev/null +++ b/scripts/tests/witness-seam-evidence.test.mjs @@ -0,0 +1,159 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mkdtemp, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { authenticateRange, phase1AGate, scoreWitnessArm, sha256, verifyPairedInputs } from "../lib/witness-seam-evidence.mjs"; +import { verifyRequiredOperation } from "../codestory-witness-seam-evaluate.mjs"; + +function fixture() { + const bytes = Buffer.from("// preamble\nfn café() { work(); }\nfn alternate() {}\n"); + const source = bytes.toString(); + const range = (text, line) => ({ path: "src/a.rs", byte_range: { start: bytes.indexOf(text), end: bytes.indexOf(text) + Buffer.byteLength(text) }, + line_range: { start: line, end: line }, content_digest: sha256(bytes) }); + const primary = range("fn café() { work(); }\n", 2); + const alternative = range("fn alternate() {}\n", 3); + return { source, bytes, sources: new Map([["src/a.rs", bytes]]), primary, + annotation: { acceptable_sets: [primary, alternative].map((atom, index) => ({ set_id: String(index), + required_source_atoms: [{ atom_id: "witness", source_range: atom }], allowed_support_ranges: [primary, alternative] })) }, + row: { kind: "source_range", path: "src/a.rs", start_line: 2, end_line: 2, snippet: "```text\n> 2 | fn café() { work(); }\n```" } }; +} + +test("evidence coverage admits independently supported alternatives and counts actual bytes", () => { + const f = fixture(); + const primary = scoreWitnessArm({ support: [f.row] }, f.annotation, f.sources); + assert.equal(primary.recall, 1); + assert.equal(primary.relevant_byte_precision, 1); + const other = { ...f.row, start_line: 3, end_line: 3, snippet: "```text\n> 3 | fn alternate() {}\n```" }; + assert.equal(scoreWitnessArm({ support: [other] }, f.annotation, f.sources).recall, 1); + const header = { ...f.row, start_line: 1, end_line: 1, snippet: "```text\n> 1 | // preamble\n```" }; + assert.equal(scoreWitnessArm({ support: [header] }, f.annotation, f.sources).irrelevant_byte_ratio, 1); + assert.equal(scoreWitnessArm({ support: [header] }, f.annotation, f.sources).recall, 0); +}); + +test("source authentication rejects fabricated, stale, aliased, and partial addresses", () => { + const f = fixture(); + for (const range of [ + { ...f.primary, content_digest: "0".repeat(64) }, + { ...f.primary, path: "a.rs" }, + { ...f.primary, path: "src/../a.rs" }, + { ...f.primary, line_range: { start: 1, end: 1 } }, + { ...f.primary, byte_range: { start: f.bytes.indexOf("é") + 1, end: f.primary.byte_range.end } }, + ]) assert.throws(() => authenticateRange(range, f.sources)); + assert.throws(() => scoreWitnessArm({ support: [{ ...f.row, snippet: f.row.snippet.replace("work", "fake") }] }, f.annotation, f.sources)); + const truncated = { ...f.row, snippet: "```text\n> 2 | fn café() {\n// ... source truncated by packet row cap\n```" }; + assert.throws(() => scoreWitnessArm({ support: [truncated] }, f.annotation, f.sources)); + const control = scoreWitnessArm({ support: [truncated] }, f.annotation, f.sources, { headerControl: true }); + assert.equal(control.recall, 0, "a partial line never covers an entire atom"); + assert.equal(control.exposed_source_bytes, Buffer.byteLength("fn café() {")); + assert.throws(() => scoreWitnessArm({ support: [{ kind: "typed_graph_edge" }] }, f.annotation, f.sources)); + assert.throws(() => scoreWitnessArm({ support: [f.row], continuation: ["x".repeat(17000)] }, f.annotation, f.sources)); + assert.throws(() => scoreWitnessArm({ support: [f.row], continuation: [], gap: "serialized_public_budget" }, f.annotation, f.sources)); +}); + +test("required operation validity binds success, invocation, streams, and artifact", async () => { + const root = await mkdtemp(path.join(tmpdir(), "witness-operation-")); + const artifact = path.join(root, "receipt.json"), bytes = "{}", digest = sha256(bytes); + const stdout = `${digest} ${artifact}\n`; + const operation = { command: ["/trusted/witness", "replay"], exit_code: 0, error: null, wall_ms: 3, + stdout_path: path.join(root, "stdout"), stderr_path: path.join(root, "stderr"), + stdout_sha256: sha256(stdout), stderr_sha256: sha256("") }; + await writeFile(artifact, bytes); + await writeFile(operation.stdout_path, stdout); + await writeFile(operation.stderr_path, ""); + const check = (value) => verifyRequiredOperation(value, operation.command, artifact, digest); + await check(operation); + for (const mutate of [ + () => null, + (copy) => ({ ...copy, exit_code: 1 }), + (copy) => ({ ...copy, error: "timed out" }), + (copy) => ({ ...copy, error: undefined }), + (copy) => ({ ...copy, command: ["/other/witness", "replay"] }), + (copy) => ({ ...copy, wall_ms: undefined }), + (copy) => ({ ...copy, stdout_sha256: "0".repeat(64) }), + (copy) => ({ ...copy, stderr_sha256: "0".repeat(64) }), + (copy) => ({ ...copy, stdout_path: undefined }), + ]) await assert.rejects(check(mutate(structuredClone(operation)))); + await writeFile(operation.stdout_path, "wrong result shape"); + await assert.rejects(check({ ...operation, stdout_sha256: sha256("wrong result shape") })); + await writeFile(operation.stdout_path, stdout); + await writeFile(artifact, "changed"); + await assert.rejects(check(operation)); +}); + +test("Phase 1A aggregates phrasings inside cases and rejects selective subsets", () => { + const rows = ["a", "b"].flatMap((case_id) => ["original", "paraphrase_1", "paraphrase_2"].map((phrasing_id) => ({ + case_id, phrasing_id, control: { recall: 0.2, irrelevant_byte_ratio: 0.8 }, + addressed: { recall: case_id === "a" ? 1 : 0.5, irrelevant_byte_ratio: 0.5 }, + }))); + const result = phase1AGate(rows, ["a", "b"]); + assert.equal(result.mean_addressed_recall, 0.75); + assert.equal(result.phase1a, "pass"); + assert.equal(result.packet_decision, "not_evaluated"); + assert.throws(() => phase1AGate(rows.slice(1), ["a", "b"])); + assert.throws(() => phase1AGate(rows.filter((row) => row.case_id === "a"), ["a", "b"])); +}); + +test("the paired-input contract binds cardinality, charge, order, and publication", () => { + const admissions = Array.from({ length: 16 }, (_, packet_ordinal) => ({ packet_ordinal, reserved_source_bytes: 512, stable_identity: `node:${packet_ordinal}` })); + const publication = { core_generation_id: "core" }; + const input = { publication, admissions, sources: admissions.map((item) => ({ stable_identity: item.stable_identity, source: "bounded" })), + relations: [], ambiguities: [], admission_gaps: [] }; + const manifest = { case_id: "a", phrasing_id: "original", publication, descriptors: admissions.map((admission) => ({ admission, anchor: { kind: "indexed_node" } })) }; + const receipt = { contract: "codestory.witness-seam-receipt/v1", case_id: "a", phrasing_id: "original", + core_pointer: { active: { generation_id: "core" } }, control: { input }, addressed: { input: structuredClone(input) } }; + verifyPairedInputs(receipt, manifest); + for (const mutate of [ + (copy) => copy.addressed.input.admissions.pop(), + (copy) => copy.addressed.input.admissions.reverse(), + (copy) => copy.addressed.input.admissions[0].reserved_source_bytes++, + (copy) => copy.addressed.input.publication.core_generation_id = "other", + (copy) => copy.addressed.input.sources[0].source = "x".repeat(513), + ]) { const copy = structuredClone(receipt); mutate(copy); assert.throws(() => verifyPairedInputs(copy, manifest)); } +}); + +test("paired retrieval exhaustion and missing precision remain observations, not invalid experiments", () => { + for (const count of [0, 1, 15, 16]) { + const admissions = Array.from({ length: count }, (_, packet_ordinal) => ({ + packet_ordinal, reserved_source_bytes: 512, stable_identity: `node:${packet_ordinal}`, + })); + const publication = { core_generation_id: "core" }; + const sources = admissions.slice(1).map(({ stable_identity }) => ({ stable_identity, source: "bounded" })); + const admission_gaps = count ? [{ kind: "source_unavailable", stable_identity: "node:0", exact_selector_ordinal: null }] : []; + const input = { publication, admissions, sources, relations: [], ambiguities: [], admission_gaps }; + const manifest = { case_id: "a", phrasing_id: "original", publication, descriptors: admissions.map((admission, i) => ({ + admission, anchor: i ? { kind: "indexed_node" } : null, + })) }; + const receipt = { contract: "codestory.witness-seam-receipt/v1", case_id: "a", phrasing_id: "original", + core_pointer: { active: { generation_id: "core" } }, control: { input }, addressed: { input: structuredClone(input) } }; + verifyPairedInputs(receipt, manifest); + if (count) { + const missing = structuredClone(receipt); + missing.addressed.input.admission_gaps = []; + assert.throws(() => verifyPairedInputs(missing, manifest), "every missing source requires a typed gap"); + const invented = structuredClone(receipt); + invented.addressed.input.sources.unshift({ stable_identity: "node:0", source: "header" }); + assert.throws(() => verifyPairedInputs(invented, manifest), "unaddressed candidates never gain source"); + } + } +}); + +test("invalid artifacts cannot create a quality aggregate or overwrite a decision", async () => { + const root = await mkdtemp(path.join(tmpdir(), "witness-evaluation-")); + const output = path.join(root, "evaluation.json"); + const script = new URL("../codestory-witness-seam-evaluate.mjs", import.meta.url); + const argv = [script.pathname, "--output", output, "--binary", process.execPath]; + for (const kind of ["questions", "annotations", "runs"]) + argv.push(`--${kind}`, path.join(root, `${kind}.json`), `--${kind}-sha256`, "0".repeat(64)); + const failed = spawnSync(process.execPath, argv, { encoding: "utf8" }); + assert.equal(failed.status, 1); + const bytes = await readFile(output, "utf8"); + const report = JSON.parse(bytes); + assert.equal(report.experiment_status, "invalid"); + assert.equal(report.phase1a, "blocked"); + assert.equal(report.packet_decision, "not_evaluated"); + assert.equal(report.mean_addressed_recall, undefined); + assert.equal(spawnSync(process.execPath, argv).status, 1); + assert.equal(await readFile(output, "utf8"), bytes); +});