From fc150dc6aa02e022ab63d4097b3eeb78c1adc1eb Mon Sep 17 00:00:00 2001 From: Agent IX Date: Sun, 11 Oct 2026 22:10:03 -0700 Subject: [PATCH 1/2] IR-496 add bounded flat schedule source generation --- src/lib.rs | 16 +- src/oracle/function/mod.rs | 233 ++++++++++++-- src/oracle/function/schedule.rs | 392 +++++++++++++++++++++++ tests/it/exact_function_generation.rs | 428 +++++++++++++++++++++++++- 4 files changed, 1036 insertions(+), 33 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 442c5457..23efd0d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,24 +76,26 @@ pub use oracle::equality::{ }; pub use oracle::function::{ - generate_exact_function_oracles, generate_function_schedules, CallPointKind, ExactFunctionBody, - ExactFunctionClaim, ExactFunctionDeclaration, ExactFunctionItem, ExactFunctionOracles, - ExactFunctionRefusal, FunctionParameter, FunctionScheduleClaim, FunctionScheduleDeclaration, + generate_exact_function_oracles, generate_function_schedules, + generate_function_schedules_with_limits, CallPointKind, ExactFunctionBody, ExactFunctionClaim, + ExactFunctionDeclaration, ExactFunctionItem, ExactFunctionOracles, ExactFunctionRefusal, + FunctionParameter, FunctionScheduleClaim, FunctionScheduleDeclaration, FunctionScheduleDisposition, FunctionScheduleGeneration, FunctionScheduleItem, - FunctionScheduleParameter, GeneratedExactFunctionClaim, LocationMapEntry, RecordedLocation, - RecordedOrigin, RefusedFunctionBody, ScheduledFunctionBody, EXACT_FUNCTION_CRATE_NAME, - EXACT_FUNCTION_LOWERING_WORK_LIMIT, + FunctionScheduleLimits, FunctionScheduleParameter, GeneratedExactFunctionClaim, + LocationMapEntry, RecordedLocation, RecordedOrigin, RefusedFunctionBody, ScheduledFunctionBody, + EXACT_FUNCTION_CRATE_NAME, EXACT_FUNCTION_LOWERING_WORK_LIMIT, }; pub use oracle::function::schedule::{ BlockId as FunctionScheduleBlockId, ControlAction as FunctionScheduleControlAction, ControlBlock as FunctionScheduleControlBlock, ControlEdge as FunctionScheduleControlEdge, EdgeCondition as FunctionScheduleEdgeCondition, Instruction as FunctionScheduleInstruction, + LocationQueryRefusal as FunctionScheduleLocationQueryRefusal, LocationRecord as FunctionScheduleLocation, Occurrence as FunctionScheduleOccurrence, Operand as FunctionScheduleOperand, Operation as FunctionScheduleOperation, Schedule as FunctionBodySchedule, ScheduleRefusal as FunctionScheduleRefusal, ScheduledOperation as FunctionScheduledOperation, SsaSlot as FunctionScheduleSlot, - ValueKind as FunctionScheduleValueKind, + ValueKind as FunctionScheduleValueKind, DEFAULT_GENERATION_WORK_LIMIT, }; pub use evidence::bound_coverage::{ diff --git a/src/oracle/function/mod.rs b/src/oracle/function/mod.rs index 7679acd4..8f39eb15 100644 --- a/src/oracle/function/mod.rs +++ b/src/oracle/function/mod.rs @@ -131,7 +131,7 @@ //! direct-expression-call shape cannot reach. AC-2's two legs (generated oracle, direct runtime //! call) are implemented and tested in full. -use crate::core::artifact::{Artifact, MAX_GENERATED_SOURCE_BYTES}; +use crate::core::artifact::{Artifact, MAX_ARTIFACT_BYTES, MAX_GENERATED_SOURCE_BYTES}; use crate::core::naming::{bounded_readable_component, unique_names}; use crate::core::profile::oracle_crate_manifest; use crate::oracle::claim::{ClaimDisposition, ClaimMap, OracleGenerationError, UpstreamBlocker}; @@ -243,8 +243,30 @@ pub struct FunctionScheduleGeneration { pub refused_functions: Vec, /// One ordered disposition per requested call item. pub claims: Vec, - /// Compilable crate source embedding the complete deterministic schedule tables. + /// Compilable crate root source embedding the deterministic schedule metadata. pub source: String, + /// Additional deterministic ordinal-range source modules for large schedules. + pub modules: Vec, + /// Total UTF-8 source bytes across the root and every module. + pub source_bytes: usize, +} + +/// Caller-selected work and source ceilings for function schedule generation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FunctionScheduleLimits { + /// Maximum charged work per function body. + pub generation_work_units: u64, + /// Maximum total generated Rust source bytes across the crate and modules. + pub source_bytes: usize, +} + +impl Default for FunctionScheduleLimits { + fn default() -> Self { + Self { + generation_work_units: schedule::DEFAULT_GENERATION_WORK_LIMIT, + source_bytes: MAX_GENERATED_SOURCE_BYTES, + } + } } /// Builds target-neutral flat body schedules from one admitted V2 graph. @@ -256,9 +278,31 @@ pub fn generate_function_schedules( declarations: &[FunctionScheduleDeclaration], items: &[FunctionScheduleItem], work_limit: u64, +) -> Result { + generate_function_schedules_with_limits( + package, + declarations, + items, + FunctionScheduleLimits { + generation_work_units: work_limit, + source_bytes: MAX_GENERATED_SOURCE_BYTES, + }, + ) +} + +/// Builds target-neutral schedules with caller-selected work and source-byte limits. +/// +/// The work ceiling applies independently to each function body. The default source ceiling is +/// 1,048,576 bytes; larger callers may raise it, while each emitted source artifact remains +/// bounded by the publication artifact limit and is split into deterministic ordinal modules. +pub fn generate_function_schedules_with_limits( + package: &CheckedPackageV2, + declarations: &[FunctionScheduleDeclaration], + items: &[FunctionScheduleItem], + limits: FunctionScheduleLimits, ) -> Result { // Reject the unrepresentable setting before package indexing or graph projection begins. - let _ = schedule::WorkCounter::new(work_limit)?; + let _ = schedule::WorkCounter::new(limits.generation_work_units)?; let source_nodes: BTreeMap = package .graph() .nodes @@ -402,7 +446,7 @@ pub fn generate_function_schedules( .map_err(|_| schedule::ScheduleRefusal::CounterOverflow) }) .collect::, _>>()?; - let mut work = schedule::WorkCounter::new(work_limit)?; + let mut work = schedule::WorkCounter::new(limits.generation_work_units)?; let result = if let Some(refusal) = lowering_failures.get(&declaration.body_node_id) { Err(refusal.clone()) } else { @@ -553,28 +597,179 @@ pub fn generate_function_schedules( .then_with(|| left.function.as_bytes().cmp(right.function.as_bytes())) }); - let mut generation = FunctionScheduleGeneration { + let (source, modules, source_bytes) = render_schedule_source( + &function_bodies, + &refused_functions, + &claims, + limits.source_bytes, + )?; + Ok(FunctionScheduleGeneration { functions: function_bodies, refused_functions, claims, - source: String::new(), - }; - let schedule_json = serde_json::to_string(&( - &generation.functions, - &generation.refused_functions, - &generation.claims, - )) - .map_err(|_| schedule::ScheduleRefusal::SerializationFailure)?; - generation.source = format!( - "/// Deterministic target-neutral function schedule.\npub const FUNCTION_SCHEDULE_JSON: &str = {schedule_json:?};\n" + source, + modules, + source_bytes, + }) +} + +const SCHEDULE_INSTRUCTIONS_PER_MODULE: usize = 1024; + +#[derive(Serialize)] +struct ScheduleSourcePart<'a> { + function_node_id: &'a CheckedNodeId, + function_name: &'a str, + instruction_start: usize, + instruction_end: usize, + instructions: &'a [schedule::Instruction], + locations: &'a [schedule::LocationRecord], + control_flow: &'a [schedule::ControlBlock], +} + +fn render_schedule_source( + functions: &[ScheduledFunctionBody], + refused_functions: &[RefusedFunctionBody], + claims: &[FunctionScheduleClaim], + source_limit: usize, +) -> Result<(String, Vec, usize), schedule::ScheduleRefusal> { + let complete_json = serde_json::to_string(&(functions, refused_functions, claims)) + .map_err(|_| schedule::ScheduleRefusal::SerializationFailure)?; + let flat_source = format!( + "/// Deterministic target-neutral function schedule.\npub const FUNCTION_SCHEDULE_JSON: &str = {complete_json:?};\n" ); - if generation.source.len() > MAX_GENERATED_SOURCE_BYTES { + if flat_source.len() <= MAX_ARTIFACT_BYTES { + let source_bytes = flat_source.len(); + if source_bytes > source_limit { + return Err(schedule::ScheduleRefusal::SourceTooLarge { + bytes: source_bytes, + limit: source_limit, + setting: "cg.function.source_bytes", + }); + } + return Ok((flat_source, Vec::new(), source_bytes)); + } + + let metadata = functions + .iter() + .map(|function| { + ( + &function.function_node_id, + &function.name, + function.schedule.entry, + function.schedule.instructions.len(), + ) + }) + .collect::>(); + let metadata_json = serde_json::to_string(&(metadata, refused_functions, claims)) + .map_err(|_| schedule::ScheduleRefusal::SerializationFailure)?; + let mut source = format!( + "/// Deterministic target-neutral function schedule metadata.\npub const FUNCTION_SCHEDULE_METADATA_JSON: &str = {metadata_json:?};\n" + ); + let mut modules = Vec::new(); + let mut declarations = String::new(); + let mut module_values = String::new(); + for (function_ordinal, function) in functions.iter().enumerate() { + let instruction_count = function.schedule.instructions.len(); + let location_count = function.schedule.locations.len(); + if instruction_count != location_count { + return Err(schedule::ScheduleRefusal::MalformedTerm( + function.function_node_id.clone(), + )); + } + let mut start = 0usize; + while start < instruction_count { + let end = start + .saturating_add(SCHEDULE_INSTRUCTIONS_PER_MODULE) + .min(instruction_count); + let block_start = start + .checked_mul(2) + .ok_or(schedule::ScheduleRefusal::CounterOverflow)?; + let mut block_end = end + .checked_mul(2) + .ok_or(schedule::ScheduleRefusal::CounterOverflow)?; + if end == instruction_count { + block_end = function.schedule.control_flow.len(); + } + let part = ScheduleSourcePart { + function_node_id: &function.function_node_id, + function_name: &function.name, + instruction_start: start, + instruction_end: end, + instructions: &function.schedule.instructions[start..end], + locations: &function.schedule.locations[start..end], + control_flow: function + .schedule + .control_flow + .get(block_start..block_end) + .ok_or_else(|| { + schedule::ScheduleRefusal::MalformedTerm(function.function_node_id.clone()) + })?, + }; + let part_json = serde_json::to_string(&part) + .map_err(|_| schedule::ScheduleRefusal::SerializationFailure)?; + let module_name = format!("schedule_{function_ordinal:06}_{start:06}_{end:06}"); + let module_source = format!( + "/// Ordinal range {start}..{end} for function schedule {function_ordinal}.\npub const FUNCTION_SCHEDULE_PART_JSON: &str = {part_json:?};\n" + ); + if module_source.len() > MAX_ARTIFACT_BYTES { + return Err(schedule::ScheduleRefusal::SourceArtifactTooLarge { + bytes: module_source.len(), + limit: MAX_ARTIFACT_BYTES, + }); + } + declarations.push_str(&format!("mod {module_name};\n")); + module_values.push_str(&format!( + " {module_name}::FUNCTION_SCHEDULE_PART_JSON,\n" + )); + modules.push(Artifact::new( + format!("src/{module_name}.rs"), + module_source, + )); + start = end; + } + } + let artifact_count = modules + .len() + .checked_add(1) + .ok_or(schedule::ScheduleRefusal::CounterOverflow)?; + if artifact_count > crate::core::artifact::MAX_ARTIFACTS { + return Err(schedule::ScheduleRefusal::TooManySourceModules { + count: artifact_count, + limit: crate::core::artifact::MAX_ARTIFACTS, + }); + } + source.push_str(&declarations); + source.push_str( + "\n/// Serialized per-range schedule tables in function and instruction order.\n", + ); + source.push_str("pub const FUNCTION_SCHEDULE_PARTS: &[&str] = &[\n"); + source.push_str(&module_values); + source.push_str("];\n"); + if source.len() > MAX_ARTIFACT_BYTES { + return Err(schedule::ScheduleRefusal::SourceArtifactTooLarge { + bytes: source.len(), + limit: MAX_ARTIFACT_BYTES, + }); + } + let source_bytes = modules.iter().try_fold(source.len(), |total, module| { + total + .checked_add(module.contents.len()) + .ok_or(schedule::ScheduleRefusal::CounterOverflow) + })?; + if source_bytes > crate::core::artifact::MAX_BUNDLE_BYTES { + return Err(schedule::ScheduleRefusal::SourceBundleTooLarge { + bytes: source_bytes, + limit: crate::core::artifact::MAX_BUNDLE_BYTES, + }); + } + if source_bytes > source_limit { return Err(schedule::ScheduleRefusal::SourceTooLarge { - bytes: generation.source.len(), - limit: MAX_GENERATED_SOURCE_BYTES, + bytes: source_bytes, + limit: source_limit, + setting: "cg.function.source_bytes", }); } - Ok(generation) + Ok((source, modules, source_bytes)) } fn project_body_graph( diff --git a/src/oracle/function/schedule.rs b/src/oracle/function/schedule.rs index 9c51fe10..fadf16ef 100644 --- a/src/oracle/function/schedule.rs +++ b/src/oracle/function/schedule.rs @@ -8,6 +8,9 @@ use quire_contract_model::CheckedNodeId; use serde::Serialize; use std::collections::{BTreeMap, BTreeSet}; +/// Default generation work limit applied independently to each function body. +pub const DEFAULT_GENERATION_WORK_LIMIT: u64 = 65_536; + /// Stable SSA slot assigned in left-to-right postorder. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct SsaSlot( @@ -162,6 +165,8 @@ pub enum ScheduledOperation { /// Constant-size source location for an operation occurrence. #[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct LocationRecord { + /// Exact V2 function declaration that owns this occurrence. + pub function: CheckedNodeId, /// Stable operation occurrence ordinal. pub occurrence: Occurrence, /// V2 source node identity. @@ -185,6 +190,99 @@ pub struct Schedule { pub control_flow: Vec, /// First block entered by an executor. pub entry: BlockId, + /// Direct occurrence-to-record index, kept out of the serialized schedule tables. + #[serde(skip)] + pub(crate) location_index_by_occurrence: Vec, +} + +/// Why a caller-selected parent-linked location query could not complete. +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub enum LocationQueryRefusal { + /// The requested work limit cannot represent its first denied count. + InvalidWorkLimit, + /// The requested occurrence is absent from this schedule. + MissingOccurrence(Occurrence), + /// A record does not retain the required child ordinal or parent record. + MalformedLocation(Occurrence), + /// The first parent link over budget, before its ordinal enters the result. + WorkExhausted { + /// Caller-selected link ceiling. + limit: u64, + /// First denied link count. + consumed: u64, + }, + /// The first path ordinal over the caller's byte ceiling. + OutputBytesExceeded { + /// Caller-selected path-byte ceiling. + limit: usize, + /// First denied cumulative output byte count. + consumed: usize, + }, + /// A work or byte counter cannot represent its next value. + CounterOverflow, +} + +impl Schedule { + /// Reconstructs the root-to-occurrence child path from compact parent links. + /// + /// Each link costs one work unit and four output bytes, the fixed-width `u32` encoding of + /// one child ordinal. No partial path is returned when either caller-selected limit refuses. + pub fn location_path( + &self, + occurrence: Occurrence, + work_limit: u64, + output_byte_limit: usize, + ) -> Result, LocationQueryRefusal> { + if work_limit == u64::MAX { + return Err(LocationQueryRefusal::InvalidWorkLimit); + } + let mut path = Vec::new(); + let mut work = 0u64; + let mut output_bytes = 0usize; + let mut current = occurrence; + loop { + let index = usize::try_from(current.0) + .ok() + .and_then(|ordinal| self.location_index_by_occurrence.get(ordinal)) + .copied() + .ok_or(LocationQueryRefusal::MissingOccurrence(current))?; + let record = self + .locations + .get(index) + .filter(|record| record.occurrence == current) + .ok_or(LocationQueryRefusal::MalformedLocation(current))?; + let Some(parent) = record.parent else { + break; + }; + let consumed_work = work + .checked_add(1) + .ok_or(LocationQueryRefusal::CounterOverflow)?; + if consumed_work > work_limit { + return Err(LocationQueryRefusal::WorkExhausted { + limit: work_limit, + consumed: consumed_work, + }); + } + let ordinal = record + .child_ordinal + .ok_or(LocationQueryRefusal::MalformedLocation(current))?; + let consumed_bytes = output_bytes + .checked_add(std::mem::size_of::()) + .ok_or(LocationQueryRefusal::CounterOverflow)?; + if consumed_bytes > output_byte_limit { + return Err(LocationQueryRefusal::OutputBytesExceeded { + limit: output_byte_limit, + consumed: consumed_bytes, + }); + } + path.push(ordinal); + work = consumed_work; + output_bytes = consumed_bytes; + current = parent; + } + path.reverse(); + Ok(path) + } } /// One block in the target-neutral charge and execution graph. @@ -337,6 +435,29 @@ pub enum ScheduleRefusal { bytes: usize, /// Default generated-source byte ceiling. limit: usize, + /// Stable setting name that supplied the effective limit. + setting: &'static str, + }, + /// One generated source artifact exceeds the publication byte ceiling. + SourceArtifactTooLarge { + /// Artifact byte count. + bytes: usize, + /// Per-artifact publication ceiling. + limit: usize, + }, + /// The generated schedule requires more source modules than a bundle can contain. + TooManySourceModules { + /// Number of required modules. + count: usize, + /// Maximum artifact count in one bundle. + limit: usize, + }, + /// Generated schedule sources exceed the artifact bundle's independent byte ceiling. + SourceBundleTooLarge { + /// Aggregate source bytes across the root and ordinal-range modules. + bytes: usize, + /// Maximum artifact-bundle bytes. + limit: usize, }, /// Typed schedule tables could not be serialized into generated source. SerializationFailure, @@ -509,6 +630,7 @@ pub(crate) fn build_schedule_with_work( let mut operations = BTreeMap::::new(); let mut instructions = Vec::new(); let mut locations = Vec::new(); + let mut location_index_by_occurrence = Vec::with_capacity(nodes.len()); let mut next_occurrence = 0u64; while let Some(event) = stack.pop() { @@ -698,7 +820,18 @@ pub(crate) fn build_schedule_with_work( occurrence, }); work.charge()?; + let location_index = usize::try_from(occurrence.0) + .ok() + .ok_or(ScheduleRefusal::CounterOverflow)?; + if location_index >= location_index_by_occurrence.len() { + let slot_count = location_index + .checked_add(1) + .ok_or(ScheduleRefusal::CounterOverflow)?; + location_index_by_occurrence.resize(slot_count, usize::MAX); + } + location_index_by_occurrence[location_index] = locations.len(); locations.push(LocationRecord { + function: current_function.declaration.clone(), occurrence, node: node.clone(), parent: parent.as_ref().map(|value| value.occurrence), @@ -717,6 +850,7 @@ pub(crate) fn build_schedule_with_work( locations, control_flow, entry, + location_index_by_occurrence, }) } @@ -903,6 +1037,264 @@ mod tests { } } + fn deep_id(value: u32) -> CheckedNodeId { + CheckedNodeId { + domain: "quire.checked-semantic-node/v1".into(), + digest: format!("{value:064x}").into(), + } + } + + #[test] + /// Trace: FR-021-AC-7, FR-021-AC-15, FR-021-AC-25, FR-021-AC-27, FR-021-AC-28, FR-021-AC-29, TC-031. + fn ac7_hundred_thousand_call_nodes_schedule_on_a_512_kib_stack() { + const CALLS: u32 = 100_000; + const CALL_COUNT: usize = 100_000; + const REQUIRED_WORK: u64 = 500_003; + + let parameter = deep_id(1); + let declaration = deep_id(0); + let integer_type = deep_id(CALLS + 2); + let reference = |target: &CheckedNodeId| { + serde_json::json!({ + "term": "reference", + "target": {"domain": target.domain, "digest": target.digest}, + }) + }; + let mut source_nodes = Vec::with_capacity(CALL_COUNT + 1); + source_nodes.push(super::super::CheckedSemanticNodeV2 { + node_id: parameter.clone(), + schema_version: "quire.checked-semantic-graph/v2".into(), + node_tag: "value".into(), + semantic_form: "parameter".into(), + semantic_type: integer_type.clone(), + dependencies: Vec::new(), + occurrences: Vec::new(), + recursion_group: None, + nominal_identity_preimage: None, + declaration: None, + owner: None, + body: serde_json::json!({}), + }); + for ordinal in 0..CALLS { + let node_id = deep_id(ordinal + 2); + let argument = if ordinal == 0 { + parameter.clone() + } else { + deep_id(ordinal + 1) + }; + source_nodes.push(super::super::CheckedSemanticNodeV2 { + node_id, + schema_version: "quire.checked-semantic-graph/v2".into(), + node_tag: "expression".into(), + semantic_form: "call".into(), + semantic_type: integer_type.clone(), + dependencies: Vec::new(), + occurrences: Vec::new(), + recursion_group: None, + nominal_identity_preimage: None, + declaration: None, + owner: None, + body: serde_json::json!({ + "term": "application", + "operator": "call", + "operation": {"identity": "quire.op.function.call"}, + "result_type": { + "domain": integer_type.domain, + "digest": integer_type.digest, + }, + "arguments": [reference(&declaration), reference(&argument)], + }), + }); + } + + let root = deep_id(CALLS + 1); + let current = FunctionBinding { + declaration: declaration.clone(), + parameter_kinds: vec![ValueKind::Integer], + result_kind: ValueKind::Integer, + }; + let functions = BTreeMap::from([(declaration.clone(), current.clone())]); + let parameters = BTreeMap::from([(parameter, 0)]); + let schedule = std::thread::Builder::new() + .name("ir496-ac7-small-stack".to_owned()) + .stack_size(512 * 1024) + .spawn(move || { + let source_node_map = source_nodes + .iter() + .map(|node| (node.node_id.clone(), node)) + .collect::>(); + let type_kinds = BTreeMap::from([(integer_type, ValueKind::Integer)]); + let mut work = + WorkCounter::new(REQUIRED_WORK).expect("the exact work limit is representable"); + let projected = super::super::project_body_graph( + &source_node_map, + &type_kinds, + &root, + &mut work, + ) + .expect("the V2 reference tree projects iteratively"); + let complete = build_schedule_with_work( + &root, + &projected, + ¶meters, + ¤t, + &functions, + &mut work, + ) + .expect("the exact work limit admits the complete deep schedule"); + assert_eq!(complete.instructions.len(), CALL_COUNT + 1); + assert_eq!(complete.locations.len(), CALL_COUNT + 1); + assert_eq!( + complete + .instructions + .iter() + .filter(|instruction| matches!( + instruction.operation, + ScheduledOperation::Call { .. } + )) + .count(), + CALL_COUNT + ); + assert!(complete.instructions.iter().all(|instruction| { + instruction.operands.iter().all(|operand| match operand { + Operand::Slot(slot) => slot.0 < instruction.output.0, + Operand::Literal { .. } => true, + }) + })); + assert_eq!( + complete + .locations + .first() + .and_then(|location| location.parent), + Some(Occurrence(99_999)) + ); + assert_eq!( + complete + .locations + .last() + .and_then(|location| location.parent), + None + ); + assert!(complete + .locations + .iter() + .all(|location| location.function == declaration)); + let path = complete + .location_path(Occurrence(100_000), 100_000, 400_000) + .expect("the exact query budgets admit the deepest parent chain"); + assert_eq!(path.len(), 100_000); + assert!(path.iter().all(|ordinal| *ordinal == 1)); + assert_eq!( + complete.location_path(Occurrence(100_000), 99_999, 400_000), + Err(LocationQueryRefusal::WorkExhausted { + limit: 99_999, + consumed: 100_000, + }) + ); + assert_eq!( + complete.location_path(Occurrence(100_000), 100_000, 399_999), + Err(LocationQueryRefusal::OutputBytesExceeded { + limit: 399_999, + consumed: 400_000, + }) + ); + + drop(projected); + let mut one_less_work = WorkCounter::new(REQUIRED_WORK - 1) + .expect("the one-less work limit is representable"); + let one_less_projected = super::super::project_body_graph( + &source_node_map, + &type_kinds, + &root, + &mut one_less_work, + ) + .expect("projection fits, leaving the last schedule action to deny"); + assert!(matches!( + build_schedule_with_work( + &root, + &one_less_projected, + ¶meters, + ¤t, + &functions, + &mut one_less_work, + ), + Err(ScheduleRefusal::WorkExhausted { + limit: 500_002, + consumed: REQUIRED_WORK, + }) + )); + + let body = super::super::ScheduledFunctionBody { + function_node_id: declaration, + name: "identity_chain".to_owned(), + schedule: complete, + }; + let functions = [body]; + let (source, modules, source_bytes) = + super::super::render_schedule_source(&functions, &[], &[], usize::MAX) + .expect("raised source limit emits all ordinal-range modules"); + assert!(source_bytes > crate::core::artifact::MAX_GENERATED_SOURCE_BYTES); + assert!(modules.len() > 1); + assert!(modules.iter().all(|module| { + module.contents.len() <= crate::core::artifact::MAX_ARTIFACT_BYTES + })); + assert!(modules[0].path.contains("000000_000000_001024")); + eprintln!( + "AC-7 V2 projection: call_nodes={CALL_COUNT}, instructions={}, source_bytes={source_bytes}, modules={}", + CALL_COUNT + 1, + modules.len() + ); + assert_eq!( + super::super::render_schedule_source( + &functions, + &[], + &[], + crate::core::artifact::MAX_GENERATED_SOURCE_BYTES, + ), + Err(ScheduleRefusal::SourceTooLarge { + bytes: source_bytes, + limit: crate::core::artifact::MAX_GENERATED_SOURCE_BYTES, + setting: "cg.function.source_bytes", + }) + ); + assert_eq!( + super::super::render_schedule_source(&functions, &[], &[], source_bytes - 1,), + Err(ScheduleRefusal::SourceTooLarge { + bytes: source_bytes, + limit: source_bytes - 1, + setting: "cg.function.source_bytes", + }) + ); + + let directory = + std::env::temp_dir().join(format!("ir496-ac7-schedule-{}", std::process::id())); + let source_directory = directory.join("src"); + let _ = std::fs::remove_dir_all(&directory); + std::fs::create_dir_all(&source_directory).expect("create source directory"); + std::fs::write(source_directory.join("lib.rs"), source) + .expect("write schedule crate root"); + for module in modules { + let path = directory.join(&module.path); + std::fs::write(path, module.contents).expect("write schedule module"); + } + let output = std::process::Command::new("rustc") + .args(["--crate-type", "lib", "--crate-name", "function_schedule"]) + .arg(source_directory.join("lib.rs")) + .arg("-o") + .arg(directory.join("libfunction_schedule.rlib")) + .output() + .expect("invoke rustc for the complete schedule crate"); + assert!( + output.status.success(), + "generated schedule crate failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + std::fs::remove_dir_all(&directory).expect("remove temporary schedule crate"); + }) + .expect("spawn the 512 KiB AC-7 stack"); + schedule.join().expect("deep schedule stays within 512 KiB"); + } + #[test] /// Trace: FR-021-AC-25, FR-021-AC-26, FR-021-AC-27, TC-031. fn ac25_nested_add_schedule_is_postorder_and_budget_refusal_publishes_nothing() { diff --git a/tests/it/exact_function_generation.rs b/tests/it/exact_function_generation.rs index deb7bc23..1c31dad4 100644 --- a/tests/it/exact_function_generation.rs +++ b/tests/it/exact_function_generation.rs @@ -9,16 +9,17 @@ use crate::exact_scalar_generation::invokes_panicking_macro; use crate::scratch_crate::runtime_dependency; use quire_contract_codegen::{ - generate_exact_function_oracles, generate_function_schedules, CallPointKind, ClaimDisposition, - ExactFunctionItem, ExactFunctionRefusal, FunctionScheduleDeclaration, - FunctionScheduleDisposition, FunctionScheduleItem, FunctionScheduleOperand as ScheduleOperand, - FunctionScheduleParameter, FunctionScheduledOperation, GeneratedExactFunctionClaim, - UpstreamBlocker, + generate_exact_function_oracles, generate_function_schedules, + generate_function_schedules_with_limits, CallPointKind, ClaimDisposition, ExactFunctionItem, + ExactFunctionRefusal, FunctionScheduleDeclaration, FunctionScheduleDisposition, + FunctionScheduleItem, FunctionScheduleLimits, FunctionScheduleOperand as ScheduleOperand, + FunctionScheduleParameter, FunctionScheduleRefusal as ScheduleRefusal, + FunctionScheduledOperation, GeneratedExactFunctionClaim, UpstreamBlocker, }; -use quire_contract_model::CheckedPackageV2; +use quire_contract_model::{derive_structural_candidates, CheckedPackageV2, CheckedSemanticNodeV2}; use std::ops::Deref; -/// Trace: FR-021-AC-34, TC-031. +/// Trace: FR-021-AC-28, FR-021-AC-34, TC-031. #[test] fn tc_031_ac34_public_strict_reader_chain_generates_compilable_flat_schedule() { use serde_json::{json, Value}; @@ -388,6 +389,34 @@ fn tc_031_ac34_public_strict_reader_chain_generates_compilable_flat_schedule() { let work_limit = required_call_work.max(required_scalar_work); let generated = generate_function_schedules(&checked, &declarations, &items, work_limit) .expect("caller-raised work limit admits the complete flat schedule"); + let generated_with_limits = generate_function_schedules_with_limits( + &checked, + &declarations, + &items, + FunctionScheduleLimits { + generation_work_units: work_limit, + source_bytes: generated.source_bytes, + }, + ) + .expect("the public source limit admits the measured complete source"); + assert_eq!(generated_with_limits, generated); + assert_eq!( + generate_function_schedules_with_limits( + &checked, + &declarations, + &items, + FunctionScheduleLimits { + generation_work_units: work_limit, + source_bytes: generated.source_bytes - 1, + }, + ), + Err(ScheduleRefusal::SourceTooLarge { + bytes: generated.source_bytes, + limit: generated.source_bytes - 1, + setting: "cg.function.source_bytes", + }), + "the public generator refuses one byte below its measured source size" + ); assert_eq!(generated.functions.len(), COUNT as usize); assert_eq!(generated.refused_functions.len(), 0); for function in &generated.functions { @@ -663,6 +692,391 @@ fn tc_031_ac34_public_strict_reader_chain_generates_compilable_flat_schedule() { let _ = fs::remove_dir_all(&directory); } +/// Trace: FR-021-AC-7, FR-021-AC-25, FR-021-AC-27, FR-021-AC-28, FR-021-AC-29, TC-031. +#[test] +fn tc_031_ac7_admitted_hundred_thousand_call_nodes_generate_on_a_512_kib_stack() { + use serde_json::{json, Value}; + use std::collections::BTreeMap; + use std::{fs, process::Command}; + const CALLS: u32 = 100_000; + const PARAMETER: u32 = 1_100_000; + const HELPER: u32 = 1_100_001; + const OUTER_PARAMETER: u32 = 1_100_002; + const OUTER: u32 = 1_100_003; + const CALL_BASE: u32 = 1_200_000; + let calls = std::env::var("IR496_TEST_CALLS") + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(CALLS); + assert!((1..=CALLS).contains(&calls)); + + let operation = json!({ + "identity": "quire.op.function.call", + "laws": [], + "mode": null, + "member": null, + "leaves": [], + }); + fn apply_seeded_ids(wire: &mut Value, replacements: &BTreeMap) { + fn rewrite(value: &mut Value, replacements: &BTreeMap) { + match value { + Value::Array(items) => { + for item in items { + rewrite(item, replacements); + } + } + Value::Object(object) => { + if object.get("domain").and_then(Value::as_str) + == Some("quire.checked-semantic-node/v1") + { + if let Some(digest) = object.get("digest").and_then(Value::as_str) { + if let Some(replacement) = replacements.get(digest) { + object.insert("digest".to_owned(), json!(replacement)); + } + } + return; + } + for member in object.values_mut() { + rewrite(member, replacements); + } + } + Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {} + } + } + rewrite(wire, replacements); + } + fn preseed_structural_candidates(wire: &mut Value, codes: &[u32]) -> BTreeMap { + let nodes = wire["semantic_graph"]["nodes"] + .as_array() + .expect("fixture graph nodes"); + let selected = codes + .iter() + .map(|code| { + let old = code_id(*code); + let node = nodes + .iter() + .find(|node| node["node_id"]["digest"].as_str() == Some(old.digest.as_ref())) + .unwrap_or_else(|| panic!("no structural candidate node for code {code}")); + serde_json::from_value::(node.clone()) + .expect("fixture node deserializes for candidate derivation") + }) + .collect::>(); + let candidates = derive_structural_candidates(&selected, 1 << 20, 1 << 20) + .expect("small fixture candidates derive within limits"); + let replacements = candidates + .nodes() + .iter() + .map(|candidate| { + let old = selected[candidate.position() as usize] + .node_id + .digest + .to_string(); + let new = candidate.candidate_node_id().digest.to_string(); + (old, new) + }) + .collect::>(); + apply_seeded_ids(wire, &replacements); + replacements + } + let body_reference = |code: u32, seeded: &BTreeMap| { + let mut target = code_id(code); + if let Some(digest) = seeded.get(target.digest.as_ref()) { + target.digest = digest.clone().into_boxed_str(); + } + json!({ + "term": "reference", + "target": {"domain": target.domain, "digest": target.digest}, + }) + }; + let mut builder = PackageBuilder::default(); + builder + .code( + T_INTEGER, + "scalar_type", + "integer", + T_INTEGER, + aggregate(vec![]), + ) + .code(T_TEXT, "scalar_type", "text", T_TEXT, aggregate(vec![])); + let (base_result, base_wire, base_ids) = builder.read_resolved(); + assert!(matches!( + base_result, + quire_contract_model::CheckedPackageV2ReadResult::Admitted(_) + )); + builder = PackageBuilder::from_wire(base_wire); + let mut seeded_ids = [T_INTEGER, T_TEXT] + .into_iter() + .filter_map(|code| { + let old = code_id(code); + let new = base_ids.resolve(&old); + (old.digest != new.digest).then(|| (old.digest.to_string(), new.digest.to_string())) + }) + .collect::>(); + builder + .code( + PARAMETER, + "value", + "parameter", + T_INTEGER, + aggregate(vec![ + bound_member("name", literal("text", "helper_input")), + bound_member("level", integer_literal(0)), + ]), + ) + .code( + OUTER_PARAMETER, + "value", + "parameter", + T_INTEGER, + aggregate(vec![ + bound_member("name", literal("text", "outer_input")), + bound_member("level", integer_literal(0)), + ]), + ); + + let mut wire = builder.raw_wire(); + apply_seeded_ids(&mut wire, &seeded_ids); + seeded_ids.extend(preseed_structural_candidates( + &mut wire, + &[PARAMETER, OUTER_PARAMETER], + )); + builder = PackageBuilder::from_wire(wire); + builder.code( + HELPER, + "function", + "pure_function", + T_INTEGER, + body_reference(PARAMETER, &seeded_ids), + ); + + let mut wire = builder.raw_wire(); + apply_seeded_ids(&mut wire, &seeded_ids); + seeded_ids.extend(preseed_structural_candidates(&mut wire, &[HELPER])); + builder = PackageBuilder::from_wire(wire); + + let integer_type = seeded_ids + .get(code_id(T_INTEGER).digest.as_ref()) + .cloned() + .unwrap_or_else(|| code_id(T_INTEGER).digest.to_string()); + let helper_reference = body_reference(HELPER, &seeded_ids); + for ordinal in 0..calls { + let child = if ordinal == 0 { + OUTER_PARAMETER + } else { + CALL_BASE + ordinal - 1 + }; + builder.application_node( + CALL_BASE + ordinal, + "call", + &integer_type, + json!({ + "term": "application", + "operator": "call", + "operation": operation.clone(), + "result_type": {"domain": NODE_DOMAIN, "digest": integer_type}, + "arguments": [helper_reference.clone(), body_reference(child, &seeded_ids)], + }), + ); + } + builder.code( + OUTER, + "function", + "pure_function", + T_INTEGER, + body_reference(CALL_BASE + calls - 1, &seeded_ids), + ); + let mut wire = builder.raw_wire(); + apply_seeded_ids(&mut wire, &seeded_ids); + seeded_ids.extend(preseed_structural_candidates(&mut wire, &[OUTER])); + builder = PackageBuilder::from_wire(wire); + eprintln!("IR-496 AC7 phase=builder-complete calls={calls}"); + + let limits = quire_contract_model::CheckedPackageReadLimits { + bytes: 1 << 30, + nodes: 110_000, + edges: 300_000, + occurrences: 500_000, + diagnostics: 10_000, + work: 100_000_000, + }; + eprintln!("IR-496 AC7 phase=admission-start"); + let (package, ids) = builder.admit_with_resolved(limits); + eprintln!( + "IR-496 AC7 phase=admission-complete admitted_v2_nodes={}", + package.graph().nodes.len() + ); + let resolved_code_id = |code| { + let mut id = code_id(code); + if let Some(digest) = seeded_ids.get(id.digest.as_ref()) { + id.digest = digest.clone().into_boxed_str(); + } + ids.resolve(&id) + }; + let parameter = |code| FunctionScheduleParameter { + value_node_id: resolved_code_id(code), + type_node_id: resolved_code_id(T_INTEGER), + }; + let declarations = vec![ + FunctionScheduleDeclaration { + function_node_id: resolved_code_id(HELPER), + body_node_id: resolved_code_id(PARAMETER), + name: "helper".to_owned(), + parameters: vec![parameter(PARAMETER)], + result_type_node_id: resolved_code_id(T_INTEGER), + }, + FunctionScheduleDeclaration { + function_node_id: resolved_code_id(OUTER), + body_node_id: ids.resolve(&code_id(CALL_BASE + calls - 1)), + name: "outer".to_owned(), + parameters: vec![parameter(OUTER_PARAMETER)], + result_type_node_id: resolved_code_id(T_INTEGER), + }, + ]; + let helper_function_node_id = resolved_code_id(HELPER); + let outer_function_node_id = resolved_code_id(OUTER); + let worker = std::thread::Builder::new() + .name("ir496-ac7-public-small-stack".to_owned()) + .stack_size(512 * 1024) + .spawn(move || { + const REQUIRED_WORK: u64 = 500_003; + eprintln!("IR-496 AC7 phase=public-default-start"); + let default_result = generate_function_schedules_with_limits( + &package, + &declarations, + &[], + FunctionScheduleLimits { + generation_work_units: REQUIRED_WORK, + source_bytes: 1_048_576, + }, + ); + let (raised, source_bytes, exceeded_default) = match default_result { + Err(ScheduleRefusal::SourceTooLarge { + bytes, + limit: 1_048_576, + setting: "cg.function.source_bytes", + }) => { + assert_eq!(calls, CALLS, "only the 100K case should exceed the default"); + eprintln!("IR-496 AC7 phase=public-default-complete bytes={bytes}"); + eprintln!("IR-496 AC7 phase=public-raised-start"); + let raised = generate_function_schedules_with_limits( + &package, + &declarations, + &[], + FunctionScheduleLimits { + generation_work_units: REQUIRED_WORK, + source_bytes: bytes, + }, + ) + .expect("the measured caller source limit admits every schedule module"); + (raised, bytes, true) + } + Ok(generated) => { + assert!(calls < CALLS, "the 100K case must exercise the source ceiling"); + let bytes = generated.source_bytes; + (generated, bytes, false) + } + other => panic!("default source behavior was unexpected: {other:?}"), + }; + assert_eq!(raised.functions.len(), 2); + let helper = raised + .functions + .iter() + .find(|function| function.function_node_id == helper_function_node_id) + .expect("the generated helper schedule is present"); + let outer = raised + .functions + .iter() + .find(|function| function.function_node_id == outer_function_node_id) + .expect("the generated outer schedule is present"); + assert_eq!( + raised + .functions + .iter() + .filter(|function| function.function_node_id == helper_function_node_id) + .count(), + 1, + "the exact helper declaration has one generated schedule" + ); + assert_eq!( + raised + .functions + .iter() + .filter(|function| function.function_node_id == outer_function_node_id) + .count(), + 1, + "the exact outer declaration has one generated schedule" + ); + let helper_instruction_count = helper.schedule.instructions.len(); + let outer_instruction_count = outer.schedule.instructions.len(); + eprintln!( + "IR-496 AC7 phase=public-raised-counts helper_instructions={helper_instruction_count} outer_instructions={outer_instruction_count}" + ); + assert_eq!(helper_instruction_count, 1); + assert_eq!(outer_instruction_count, calls as usize + 1); + assert_eq!(outer.schedule.locations.len(), calls as usize + 1); + assert_eq!(raised.source_bytes, source_bytes); + if exceeded_default { + assert!(raised.source_bytes > 1_048_576); + assert!(raised.modules.len() > 1); + } else { + assert!(raised.source_bytes <= 1_048_576); + } + let measured_bytes = raised.modules.iter().fold(raised.source.len(), |total, module| { + total + module.contents.len() + }); + assert_eq!(measured_bytes, raised.source_bytes); + assert!(raised.source.len() <= 16 * 1024 * 1024); + assert!(raised + .modules + .iter() + .all(|module| module.contents.len() <= 16 * 1024 * 1024)); + + let directory = std::env::temp_dir().join(format!( + "ir496-ac7-generated-schedule-{}", + std::process::id() + )); + let _ = fs::remove_dir_all(&directory); + let source_directory = directory.join("src"); + fs::create_dir_all(&source_directory).expect("create generated schedule source dir"); + let root_source = source_directory.join("lib.rs"); + fs::write(&root_source, &raised.source).expect("write generated schedule root"); + for module in &raised.modules { + let relative = module + .path + .strip_prefix("src/") + .expect("schedule module path is rooted under src"); + let path = source_directory.join(relative); + fs::create_dir_all(path.parent().expect("module path has a parent")) + .expect("create generated module directory"); + fs::write(path, &module.contents).expect("write generated schedule module"); + } + let output = Command::new("rustc") + .args(["--crate-type", "lib", "--crate-name", "function_schedule"]) + .arg(&root_source) + .arg("-o") + .arg(directory.join("libfunction_schedule.rlib")) + .output() + .expect("invoke rustc for generated schedule crate"); + assert!( + output.status.success(), + "generated schedule crate compile failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let _ = fs::remove_dir_all(&directory); + eprintln!("IR-496 AC7 phase=public-raised-complete"); + eprintln!( + "AC-7 admitted V2 public generation: call_nodes={calls}, instructions={}, source_bytes={}, modules={}", + outer_instruction_count, + raised.source_bytes, + raised.modules.len() + ); + }) + .expect("spawn the public generation worker with a 512 KiB stack"); + worker + .join() + .expect("admitted deep generation fits 512 KiB"); +} + /// Trace: FR-021-AC-23, TC-031. #[test] fn tc_031_application_candidate_repair_admits_parent_after_child_key_changes() { From a7f25276dbc40fda8efb1ca50e44bc9b4c873903 Mon Sep 17 00:00:00 2001 From: Agent IX Date: Sun, 11 Oct 2026 23:10:50 -0700 Subject: [PATCH 2/2] Record IR-496 current-head review --- reviews/ir-496-code-review.md | 12 +++++++++++- reviews/ir-496-gap-analysis.md | 17 ++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/reviews/ir-496-code-review.md b/reviews/ir-496-code-review.md index e689b5ba..66848565 100644 --- a/reviews/ir-496-code-review.md +++ b/reviews/ir-496-code-review.md @@ -4,7 +4,7 @@ title: "IR-496 PR 367 code review and Rust lane" type: SpecReview analysis: code-review review_set: subset -scope: "agent-ix/quire-contract-codegen@0ce345ff1fb66f6bd9c5a445aeb6411cb9720a4b; src/lib.rs, src/oracle/function/mod.rs, src/oracle/function/schedule.rs, tests/checked_package_support/rekey.rs, tests/it/exact_function_generation.rs; FR-021-AC-24..27, AC-34" +scope: "agent-ix/quire-contract-codegen@fc150dc6aa02e022ab63d4097b3eeb78c1adc1eb; src/lib.rs, src/oracle/function/mod.rs, src/oracle/function/schedule.rs, tests/checked_package_support/rekey.rs, tests/it/exact_function_generation.rs; FR-021-AC-24..27, AC-34" relationships: - { target: "ix://agent-ix/quire-contract-codegen/FR-021", type: references } --- @@ -38,3 +38,13 @@ Reviewed public PR #367 at `0ce345ff1fb66f6bd9c5a445aeb6411cb9720a4b` against `o - FND-006: fixed at `6b61567cea79b1e246d6a87eb5941979c6e650a`. The same-reviewer delta review of `6b61567cea79b1e246d6a87eb5941979c6e650a` found all six findings fixed. This scoped seed does not claim FR-021-AC-7's 100K schedule acceptance or AC-28. + +## Current-head review record + +Reviewed exact clean head `fc150dc6aa02e022ab63d4097b3eeb78c1adc1eb` against `origin/main` at `701ac65f8b8360236b1b48ca3f549d726a180100`. The diff contains `src/lib.rs`, `src/oracle/function/mod.rs`, `src/oracle/function/schedule.rs`, and `tests/it/exact_function_generation.rs`. The merged IR-764 checked-package support is base context, not a changed file. + +**Current-head verdict: PASS — no new code or Rust findings.** The original FND-001..006 rows and their fixed dispositions above are historical and remain unchanged. Source review confirms the current public path meters projection node/edge work before scheduling; preserves scalar literal values and equality operation identities; sorts claims by declaration identity before names and deduplicates requests; and includes explicit charge-boundary control-flow operations. The new source-limit path checks total root-plus-module bytes before returning a generation result, enforces per-artifact and bundle ceilings, and partitions large schedules into deterministic ordinal modules. The 100,000-call tests include exact and one-less work/query/source controls, a 512 KiB worker stack, and generated-crate compilation assertions. + +Current acceptance criteria examined: FR-021-AC-7 (deep 100K schedule, raised source limit and compile), AC-15 (constant-size location records and charged path query), AC-24 (claim ordering/deduplication), AC-25 (pre-action charges and all-or-nothing schedule), AC-26 (left-stop charge order), AC-27 (bound calls/cycle/shared-node refusals), AC-28 (source ceiling and raised-limit outputs), AC-29 (linear parent-chain path), and AC-34 (strict-reader chain control). AC-18 was read as context only; its QSL agreement leg remains outside this diff. + +No Cargo, Kani, integration test, or other gate was run, per dispatch. This is source review only; assertions in the test source were inspected but not executed. diff --git a/reviews/ir-496-gap-analysis.md b/reviews/ir-496-gap-analysis.md index 09ee1836..5099c5e0 100644 --- a/reviews/ir-496-gap-analysis.md +++ b/reviews/ir-496-gap-analysis.md @@ -3,7 +3,7 @@ id: SR-8641 title: "IR-496 PR 367 gap analysis" type: SpecReview analysis: gap-analysis -scope: "agent-ix/quire-contract-codegen@0ce345ff1fb66f6bd9c5a445aeb6411cb9720a4b; spec/, src/, tests/" +scope: "agent-ix/quire-contract-codegen@fc150dc6aa02e022ab63d4097b3eeb78c1adc1eb; spec/, src/, tests/" review_set: subset relationships: - { target: "ix://agent-ix/quire-contract-codegen/FR-021", type: references } @@ -22,6 +22,7 @@ Planless repository matrix and reverse-gap audit, with focused source/test inspe | ID | Severity | Summary | Refs | | --- | --- | --- | --- | | FND-001 | high | The computed matrix has 323 untagged criteria; the changed FR-021 slice leaves AC-26 and AC-28 without a bound test | FR-021-AC-26, FR-021-AC-28 | +| FND-002 | high | The repository-wide computed matrix has 318 untagged criteria; AC-18 remains an untagged QSL-owned planned criterion outside this diff, and no changed-scope FR-021 criterion is untagged | computed matrix; FR-021-AC-18 | ## Coverage @@ -36,3 +37,17 @@ Planless repository matrix and reverse-gap audit, with focused source/test inspe - Changed-scope AC-26: fixed at `6b61567cea79b1e246d6a87eb5941979c6e650a`; explicit charge-before-action branches and denied-charge-to-Stop behavior are covered. - AC-28: deferred outside this scoped seed PR. The source ceiling remains unchanged, and this PR does not claim raised-limit/deep-body acceptance. - Repository-wide matrix: the original 323-untagged-criteria FAIL is retained from the review baseline and was not recomputed for this delta. It is not evidence that this scoped PR introduces 323 gaps. + +## Current-head audit record + +Exact clean head `fc150dc6aa02e022ab63d4097b3eeb78c1adc1eb` was audited against base `701ac65f8b8360236b1b48ca3f549d726a180100`; production change scope is the function schedule generator and its integration/unit coverage. The computed Quoin matrix has 738 criteria: 368 tagged, 318 untagged, 11 tagged-by-ignored-test, and 41 method-without-symbol. This aggregate remains a repository-wide FAIL, with many untagged criteria outside the changed feature. The historical FND-001 count of 323 and its disposition above are preserved as the PR #367 baseline record. + +**Current changed-scope assessment: PASS for the reviewed FR-021 schedule slice.** The current computed matrix tags FR-021-AC-7, AC-15, AC-24, AC-25, AC-26, AC-27, AC-28, AC-29, and AC-34. AC-18 is still untagged, but its QSL-owned agreement is explicitly PLANNED and this diff does not claim that leg. The new public integration test directly exercises AC-7/AC-28 with an authentic checked graph, a 512 KiB generation worker, default source-limit refusal, caller-raised exact source limit, exact/one-less limits, module artifact ceilings, and compilation assertions. Static review confirms the tagged test builds the graph through strict admission and invokes the public API. + +The original changed-scope AC-26 disposition remains fixed at `6b61567cea79b1e246d6a87eb5941979c6e650a`; AC-28's prior deferred disposition is superseded for this current head by the new AC-28 coverage in this changed scope. The global matrix result is still FAIL; current changed-scope gaps identified: none. Reverse-gap source review: new public limits/API and module output behavior are owned by FR-021; changed source/test stubs found: 0. + +- Reconciliation evidence: `quoin matrix --repo /home/peter/dev/worktrees/ir496-deep-schedule --json` (captured as `/tmp/ir496-review-fc150dc/quoin-matrix.json`) and `quire coverage --scope /home/peter/dev/worktrees/ir496-deep-schedule --json` (captured as `/tmp/ir496-review-fc150dc/quire-coverage.json`). Quoin reports no run evidence. +- Reverse-gap inventory was bounded to the changed public API, implementation and test paths; no unowned changed behavior or stubs were identified. +- Plan completion: not assessed +- Semantic review: skipped; not requested. +- No Cargo/Kani/tests/full gates were run.