diff --git a/core/Cargo.lock b/core/Cargo.lock index d28d739cd..5b8a4cdc4 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -1501,6 +1501,7 @@ dependencies = [ "serde_json", "sha2", "solstone-core-journal-io", + "solstone-core-processing-record", "solstone-core-segment", ] @@ -1536,6 +1537,14 @@ dependencies = [ "solstone-core-segment", ] +[[package]] +name = "solstone-core-processing-record" +version = "1.0.22" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "solstone-core-segment" version = "1.0.22" diff --git a/core/Cargo.toml b/core/Cargo.toml index 7445e2895..158ee5c44 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,6 +9,7 @@ members = [ "crates/solstone-core-journal", "crates/solstone-core-journal-io", "crates/solstone-core-callosum", + "crates/solstone-core-processing-record", "crates/solstone-core-segment", "crates/solstone-core-sol", "crates/solstone-core-sol-client", @@ -42,6 +43,7 @@ solstone-core-ingest-resolve = { path = "crates/solstone-core-ingest-resolve" } solstone-core-journal = { path = "crates/solstone-core-journal" } solstone-core-journal-io = { path = "crates/solstone-core-journal-io" } solstone-core-callosum = { path = "crates/solstone-core-callosum" } +solstone-core-processing-record = { path = "crates/solstone-core-processing-record" } solstone-core-segment = { path = "crates/solstone-core-segment" } solstone-core-sol = { path = "crates/solstone-core-sol" } solstone-core-sol-client = { path = "crates/solstone-core-sol-client" } diff --git a/core/crates/solstone-core-ingest-resolve/Cargo.toml b/core/crates/solstone-core-ingest-resolve/Cargo.toml index 356314280..b3028970b 100644 --- a/core/crates/solstone-core-ingest-resolve/Cargo.toml +++ b/core/crates/solstone-core-ingest-resolve/Cargo.toml @@ -16,6 +16,7 @@ serde.workspace = true serde_json.workspace = true sha2.workspace = true solstone-core-journal-io.workspace = true +solstone-core-processing-record.workspace = true solstone-core-segment.workspace = true [lints] diff --git a/core/crates/solstone-core-ingest-resolve/src/resolve.rs b/core/crates/solstone-core-ingest-resolve/src/resolve.rs index a5a7a41ff..b9b08ae09 100644 --- a/core/crates/solstone-core-ingest-resolve/src/resolve.rs +++ b/core/crates/solstone-core-ingest-resolve/src/resolve.rs @@ -573,12 +573,16 @@ mod tests { } fn resolve_with_processing_record(record: Value) -> ApplyPlan { + resolve_with_processing_record_for_file("audio.flac", record) + } + + fn resolve_with_processing_record_for_file(name: &str, record: Value) -> ApplyPlan { let temporary = root(); let journal = temporary.path.join("journal"); let bytes = b"sound"; let directory = segment(&journal, "20260804", "device", "120000_60"); fs::write( - directory.join("audio.jsonl"), + directory.join(Path::new(name).with_extension("jsonl")), json!({"_solstone_processing":record}).to_string() + "\nsecond\n", ) .unwrap(); @@ -589,7 +593,7 @@ mod tests { "20260804", "device", "120000_60", - &[file("audio.flac", bytes)], + &[file(name, bytes)], ) .unwrap(), ) @@ -787,6 +791,48 @@ mod tests { assert_eq!(result.status, PlanStatus::Ok); } + #[test] + fn terminal_proof_refuses_stemmed_video_handler_mismatch() { + let result = resolve_with_processing_record_for_file( + "video.mp4", + json!({ + "schema":"solstone.processing.v1", + "state":"analyzed", + "handler":"transcribe", + "input_size":5, + }), + ); + + assert_eq!( + result.files[0].disposition, + FileDisposition::NeedsWrite { + reason: MissingWriteReason::MissingContent + } + ); + assert_eq!(result.status, PlanStatus::Ok); + } + + #[test] + fn terminal_proof_refuses_stemmed_audio_handler_mismatch() { + let result = resolve_with_processing_record_for_file( + "audio.flac", + json!({ + "schema":"solstone.processing.v1", + "state":"analyzed", + "handler":"describe", + "input_size":5, + }), + ); + + assert_eq!( + result.files[0].disposition, + FileDisposition::NeedsWrite { + reason: MissingWriteReason::MissingContent + } + ); + assert_eq!(result.status, PlanStatus::Ok); + } + #[test] fn missing_content_outranks_sidecar_conflict() { let temporary = root(); diff --git a/core/crates/solstone-core-ingest-resolve/src/terminal_proof.rs b/core/crates/solstone-core-ingest-resolve/src/terminal_proof.rs index 337b12b61..1717a4327 100644 --- a/core/crates/solstone-core-ingest-resolve/src/terminal_proof.rs +++ b/core/crates/solstone-core-ingest-resolve/src/terminal_proof.rs @@ -5,10 +5,10 @@ use std::fs::File; use std::io::Read; use serde_json::Value; +use solstone_core_processing_record::{TerminalProofOutcome, evaluate_terminal_proof}; use solstone_core_segment::{ContentName, SegmentDir, TerminalProofVerifier}; const MAX_FIRST_ROW_BYTES: usize = 64 * 1024; -const PROCESSING_SCHEMA: &str = "solstone.processing.v1"; /// Read-only terminal-processing verifier bound to one resolved segment. pub struct SegmentTerminalProof<'a> { @@ -51,16 +51,10 @@ impl TerminalProofVerifier for SegmentTerminalProof<'_> { let Ok(Value::Object(row)) = serde_json::from_str::(first_line) else { return false; }; - let Some(record) = row.get("_solstone_processing").and_then(Value::as_object) else { - return false; - }; - record.get("schema").and_then(Value::as_str) == Some(PROCESSING_SCHEMA) - && matches!( - record.get("state").and_then(Value::as_str), - Some("analyzed" | "empty") - ) - && record.get("handler").and_then(Value::as_str) == Some(expected_handler) - && record.get("input_size").and_then(Value::as_u64) == Some(size) + matches!( + evaluate_terminal_proof(row.get("_solstone_processing"), expected_handler, size,), + TerminalProofOutcome::Held + ) } } diff --git a/core/crates/solstone-core-processing-record/Cargo.toml b/core/crates/solstone-core-processing-record/Cargo.toml new file mode 100644 index 000000000..3e99a9a04 --- /dev/null +++ b/core/crates/solstone-core-processing-record/Cargo.toml @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +[package] +name = "solstone-core-processing-record" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/core/crates/solstone-core-processing-record/schema/processing-record.v1.schema.json b/core/crates/solstone-core-processing-record/schema/processing-record.v1.schema.json new file mode 100644 index 000000000..0258074cb --- /dev/null +++ b/core/crates/solstone-core-processing-record/schema/processing-record.v1.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "solstone-journal-format:processing-record.v1", + "title": "Solstone processing record v1", + "description": "Examples are hand-verified against this schema at authoring time; no automated Rust validator is wired up in this wave.", + "type": "object", + "required": ["schema", "state", "reason_code", "handler", "attempted_at", "input_size"], + "additionalProperties": true, + "properties": { + "schema": {"const": "solstone.processing.v1"}, + "state": {"enum": ["analyzed", "empty", "failed"]}, + "reason_code": {"enum": ["ok", "no_decodable_frames", "no_decodable_audio", "corrupt_input", "analysis_failed"]}, + "handler": {"enum": ["describe", "transcribe"]}, + "attempted_at": {"type": "string"}, + "input_size": {"type": "integer", "minimum": 0}, + "attempts": {"type": "integer"}, + "source": {"type": "string"} + }, + "examples": [ + {"schema": "solstone.processing.v1", "state": "analyzed", "reason_code": "ok", "handler": "transcribe", "attempted_at": "2026-07-25T05:14:46Z", "input_size": 5}, + {"schema": "solstone.processing.v1", "state": "empty", "reason_code": "no_decodable_audio", "handler": "transcribe", "attempted_at": "2026-07-25T05:14:46Z", "input_size": 0}, + {"schema": "solstone.processing.v1", "state": "failed", "reason_code": "corrupt_input", "handler": "transcribe", "attempted_at": "2026-07-25T05:14:46Z", "input_size": 5, "attempts": 1}, + {"schema": "solstone.processing.v1", "state": "empty", "reason_code": "no_decodable_frames", "handler": "describe", "attempted_at": "2026-07-25T05:14:46Z", "input_size": 0, "source": "backfill"} + ], + "x-journal-contract": { + "format_id": "solstone-processing-record.v1", + "schema_owner": "solstone-core-processing-record", + "reference_writer": "solstone.observe.processing_record", + "allowed_producers": ["observe.describe", "observe.transcribe"], + "write_discipline": "embedded metadata-header field; enclosing formats own file paths", + "file_kind": "embedded_header_record", + "key_fields": ["schema", "state", "reason_code", "handler", "attempted_at", "input_size"], + "header_field_of": ["screen-jsonl", "audio-jsonl"] + }, + "x-known-asymmetries": [ + "Observed, not decided: backfill-stamped empty records with source backfill grant terminal proof and retention purge eligibility without a handler having run; source is not checked by current proof implementations.", + "Observed, not decided: should_reenter_analysis_output applies the failed-attempt bound only to describe outputs, never to audio/transcribe failures.", + "Observed, not decided: transcribe non-decode failure paths write no _solstone_processing record, so those inputs are re-processed indefinitely.", + "Observed, not decided: retention reads this record through derive_modality_state using only state, plus is_failure_exhausted for failed records; unlike terminal proof, it does not check schema, handler, or input_size." + ] +} diff --git a/core/crates/solstone-core-processing-record/src/lib.rs b/core/crates/solstone-core-processing-record/src/lib.rs new file mode 100644 index 000000000..7f21d7318 --- /dev/null +++ b/core/crates/solstone-core-processing-record/src/lib.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (c) 2026 sol pbc + +//! Shared vocabulary and lenient predicates for `_solstone_processing` records. + +pub mod predicate; +pub mod vocab; + +pub use predicate::{ + TerminalProofOutcome, evaluate_terminal_proof, is_failure_exhausted, record_attempts, +}; diff --git a/core/crates/solstone-core-processing-record/src/predicate.rs b/core/crates/solstone-core-processing-record/src/predicate.rs new file mode 100644 index 000000000..8940c1657 --- /dev/null +++ b/core/crates/solstone-core-processing-record/src/predicate.rs @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (c) 2026 sol pbc + +use serde_json::Value; + +use crate::vocab; + +/// The reason a record did or did not establish terminal processing proof. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TerminalProofOutcome { + Held, + RecordAbsent, + SchemaUnrecognized, + Refused, +} + +/// Evaluate a processing record without requiring a complete typed schema. +pub fn evaluate_terminal_proof( + record: Option<&Value>, + expected_handler: &str, + input_size: u64, +) -> TerminalProofOutcome { + let Some(record) = record.and_then(Value::as_object) else { + return TerminalProofOutcome::RecordAbsent; + }; + if record.get("schema").and_then(Value::as_str) != Some(vocab::SCHEMA) { + return TerminalProofOutcome::SchemaUnrecognized; + } + if matches!( + record.get("state").and_then(Value::as_str), + Some(vocab::STATE_ANALYZED | vocab::STATE_EMPTY) + ) && record.get("handler").and_then(Value::as_str) == Some(expected_handler) + && record.get("input_size").and_then(Value::as_u64) == Some(input_size) + { + TerminalProofOutcome::Held + } else { + TerminalProofOutcome::Refused + } +} + +/// Return the failure-attempt count, preserving valid negative JSON integers. +pub fn record_attempts(record: &Value) -> i64 { + // `as_i64` rejects bool/string/null/non-integral floats, matching Python's + // integer guard including its explicit bool rejection. + record.get("attempts").and_then(Value::as_i64).unwrap_or(0) +} + +/// Return whether a failed processing record has reached terminal exhaustion. +pub fn is_failure_exhausted(record: &Value) -> bool { + if record.get("state").and_then(Value::as_str) != Some(vocab::STATE_FAILED) { + return false; + } + if record.get("reason_code").and_then(Value::as_str) == Some(vocab::REASON_CORRUPT_INPUT) { + return true; + } + record_attempts(record) >= vocab::FAILED_ATTEMPT_BOUND +} + +#[cfg(test)] +mod tests { + use serde_json::{Value, json}; + + use super::{ + TerminalProofOutcome, evaluate_terminal_proof, is_failure_exhausted, record_attempts, + }; + use crate::vocab; + + fn expected_outcome(value: &str) -> TerminalProofOutcome { + match value { + "held" => TerminalProofOutcome::Held, + "record_absent" => TerminalProofOutcome::RecordAbsent, + "schema_unrecognized" => TerminalProofOutcome::SchemaUnrecognized, + "refused" => TerminalProofOutcome::Refused, + other => panic!("unknown terminal-proof outcome {other}"), + } + } + + #[test] + fn schema_matches_documented_python_value() { + // Equality with solstone/observe/processing_record.py:23 was confirmed by inspection at + // lode time; nothing mechanically enforces cross-language equality. + assert_eq!(vocab::SCHEMA, "solstone.processing.v1"); + } + + #[test] + fn ingest_resolve_does_not_redeclare_processing_schema() { + let source = include_str!("../../solstone-core-ingest-resolve/src/terminal_proof.rs"); + assert!(!source.contains("solstone.processing.v1")); + assert!(!source.contains("PROCESSING_SCHEMA")); + } + + #[test] + fn vectors_match_processing_record_predicates() { + let fixture: Value = serde_json::from_str(include_str!( + "../tests/vectors/processing-record-vectors.json" + )) + .expect("processing-record vectors must be JSON"); + + for row in fixture["terminal_proof"] + .as_array() + .expect("terminal_proof must be an array") + { + let name = row["name"].as_str().expect("vector name must be a string"); + let expected_handler = row["expected_handler"] + .as_str() + .expect("expected_handler must be a string"); + let input_size = row["input_size"] + .as_u64() + .expect("input_size must be a u64"); + let expected = expected_outcome( + row["expected_verdict"] + .as_str() + .expect("expected_verdict must be a string"), + ); + assert_eq!( + evaluate_terminal_proof(row.get("record"), expected_handler, input_size), + expected, + "terminal-proof vector {name}", + ); + } + + for row in fixture["failure_exhaustion"] + .as_array() + .expect("failure_exhaustion must be an array") + { + let name = row["name"].as_str().expect("vector name must be a string"); + let record = row + .get("record") + .expect("failure vector record is required"); + let expected = row["expected_exhausted"] + .as_bool() + .expect("expected_exhausted must be a bool"); + assert_eq!( + is_failure_exhausted(record), + expected, + "failure-exhaustion vector {name}", + ); + } + } + + #[test] + fn unknown_record_fields_round_trip_without_loss() { + let record = json!({ + "schema": vocab::SCHEMA, + "state": vocab::STATE_ANALYZED, + "handler": vocab::HANDLER_TRANSCRIBE, + "input_size": 5, + "operator_note": "x", + }); + let serialized = serde_json::to_string(&record).unwrap(); + let reparsed: Value = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(reparsed["operator_note"], "x"); + } + + #[test] + fn negative_attempt_count_is_preserved() { + assert_eq!(record_attempts(&json!({"attempts": -1})), -1); + } +} diff --git a/core/crates/solstone-core-processing-record/src/vocab.rs b/core/crates/solstone-core-processing-record/src/vocab.rs new file mode 100644 index 000000000..c3eee6882 --- /dev/null +++ b/core/crates/solstone-core-processing-record/src/vocab.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (c) 2026 sol pbc + +// Parity: solstone/observe/processing_record.py:23 (documented only, not mechanically enforced). +pub const SCHEMA: &str = "solstone.processing.v1"; +// Parity: solstone/observe/processing_record.py:24 (documented only, not mechanically enforced). +pub const FAILED_ATTEMPT_BOUND: i64 = 3; + +// Parity: solstone/observe/processing_record.py:31. +pub const STATE_ANALYZED: &str = "analyzed"; +// Parity: solstone/observe/processing_record.py:32. +pub const STATE_EMPTY: &str = "empty"; +// Parity: solstone/observe/processing_record.py:33. +pub const STATE_FAILED: &str = "failed"; + +// Parity: solstone/observe/processing_record.py:37. +pub const REASON_OK: &str = "ok"; +// Parity: solstone/observe/processing_record.py:38. +pub const REASON_NO_DECODABLE_FRAMES: &str = "no_decodable_frames"; +// Parity: solstone/observe/processing_record.py:39. +pub const REASON_NO_DECODABLE_AUDIO: &str = "no_decodable_audio"; +// Parity: solstone/observe/processing_record.py:40. +pub const REASON_CORRUPT_INPUT: &str = "corrupt_input"; +// Parity: solstone/observe/processing_record.py:41. +pub const REASON_ANALYSIS_FAILED: &str = "analysis_failed"; + +// Parity: solstone/observe/processing_record.py:44. +pub const HANDLER_DESCRIBE: &str = "describe"; +// Parity: solstone/observe/processing_record.py:45. +pub const HANDLER_TRANSCRIBE: &str = "transcribe"; diff --git a/core/crates/solstone-core-processing-record/tests/vectors/processing-record-vectors.json b/core/crates/solstone-core-processing-record/tests/vectors/processing-record-vectors.json new file mode 100644 index 000000000..1b998b242 --- /dev/null +++ b/core/crates/solstone-core-processing-record/tests/vectors/processing-record-vectors.json @@ -0,0 +1,241 @@ +{ + "_provenance": { + "generated_from_commit": "87b9b426a5077b8bd3a91824d551b256d27bd1c3", + "equivalence_proof": "Verdicts are read from the Python reference and encoded here; tests never execute Python. Non-synthetic producer/reason pairings preserve describe for analysis_failed and no_decodable_frames, and transcribe for no_decodable_audio and corrupt_input." + }, + "terminal_proof": [ + { + "name": "minimal_terminal_record", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v1", "state": "analyzed", "handler": "transcribe", "input_size": 5}, + "expected_verdict": "held", + "provenance_tag": "constructed", + "citation": "solstone/apps/observer/processing_proof.py:64-76; solstone/observe/processing_record.py:178-185" + }, + { + "name": "unknown_state_is_refused", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v1", "state": "bogus", "handler": "transcribe", "input_size": 5}, + "expected_verdict": "refused", + "provenance_tag": "synthetic", + "citation": "solstone/apps/observer/processing_proof.py:64-76" + }, + { + "name": "unknown_reason_is_not_a_terminal_proof_condition", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v1", "state": "analyzed", "reason_code": "unknown_reason", "handler": "transcribe", "input_size": 5}, + "expected_verdict": "held", + "provenance_tag": "constructed", + "citation": "solstone/apps/observer/processing_proof.py:64-76" + }, + { + "name": "unknown_schema_is_distinguished", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v0", "state": "analyzed", "handler": "transcribe", "input_size": 5}, + "expected_verdict": "schema_unrecognized", + "provenance_tag": "synthetic", + "citation": "solstone/apps/observer/processing_proof.py:64-65" + }, + { + "name": "absent_record", + "expected_handler": "describe", + "input_size": 5, + "record": null, + "expected_verdict": "record_absent", + "provenance_tag": "observed", + "citation": "solstone/apps/observer/processing_proof.py:61-63" + }, + { + "name": "observed_79_record", + "expected_handler": "transcribe", + "input_size": 514158, + "record": {"schema": "solstone.processing.v1", "state": "analyzed", "reason_code": "ok", "handler": "transcribe", "attempted_at": "2026-07-25T05:14:46Z", "input_size": 514158}, + "expected_verdict": "held", + "provenance_tag": "observed", + "citation": "externally supplied real-journal data (79-record corpus); not derivable from this repository" + }, + { + "name": "observed_11_recordless_screen_header_before_backfill", + "expected_handler": "describe", + "input_size": 0, + "header": {"raw": ""}, + "record": null, + "expected_verdict": "record_absent", + "provenance_tag": "observed", + "citation": "externally supplied real-journal data (11-record corpus); solstone/apps/observer/processing_proof.py:61-63" + }, + { + "name": "recordless_screen_header_after_backfill", + "expected_handler": "describe", + "input_size": 123, + "record": {"schema": "solstone.processing.v1", "state": "empty", "reason_code": "no_decodable_frames", "handler": "describe", "attempted_at": "2026-07-25T05:14:46Z", "input_size": 123, "source": "backfill"}, + "expected_verdict": "held", + "provenance_tag": "constructed", + "citation": "solstone/think/backfill_processing_records.py:170-198" + }, + { + "name": "backfill_without_sibling_uses_zero_size", + "expected_handler": "describe", + "input_size": 0, + "record": {"schema": "solstone.processing.v1", "state": "empty", "reason_code": "no_decodable_frames", "handler": "describe", "attempted_at": "2026-07-25T05:14:46Z", "input_size": 0, "source": "backfill"}, + "expected_verdict": "held", + "provenance_tag": "constructed", + "citation": "solstone/think/backfill_processing_records.py:170-176; recorded-not-resolved asymmetry #1 is pinned, not resolved" + }, + { + "name": "attempted_at_is_not_parsed", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v1", "state": "analyzed", "handler": "transcribe", "attempted_at": "not-a-date", "input_size": 5}, + "expected_verdict": "held", + "provenance_tag": "synthetic", + "citation": "solstone/apps/observer/processing_proof.py:64-76" + }, + { + "name": "unknown_top_level_field_is_ignored", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v1", "state": "analyzed", "handler": "transcribe", "input_size": 5, "operator_note": "x"}, + "expected_verdict": "held", + "provenance_tag": "synthetic", + "citation": "solstone/apps/observer/processing_proof.py:64-76" + }, + { + "name": "empty_audio_no_decodable_audio", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v1", "state": "empty", "reason_code": "no_decodable_audio", "handler": "transcribe", "input_size": 5}, + "expected_verdict": "held", + "provenance_tag": "constructed", + "citation": "solstone/observe/processing_record.py:35-41; solstone/apps/observer/processing_proof.py:64-76" + }, + { + "name": "input_size_bool_is_refused", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v1", "state": "analyzed", "handler": "transcribe", "input_size": true}, + "expected_verdict": "refused", + "provenance_tag": "synthetic", + "citation": "solstone/apps/observer/processing_proof.py:71-76; Python explicitly rejects bool while Rust as_u64 rejects it, yielding the same verdict" + }, + { + "name": "input_size_string_is_refused", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v1", "state": "analyzed", "handler": "transcribe", "input_size": "5"}, + "expected_verdict": "refused", + "provenance_tag": "synthetic", + "citation": "solstone/apps/observer/processing_proof.py:71-76; Rust as_u64 rejects strings, yielding the same verdict" + }, + { + "name": "input_size_null_is_refused", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v1", "state": "analyzed", "handler": "transcribe", "input_size": null}, + "expected_verdict": "refused", + "provenance_tag": "synthetic", + "citation": "solstone/apps/observer/processing_proof.py:71-76; Rust as_u64 rejects null, yielding the same verdict" + }, + { + "name": "input_size_float_is_refused", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v1", "state": "analyzed", "handler": "transcribe", "input_size": 5.0}, + "expected_verdict": "refused", + "provenance_tag": "synthetic", + "citation": "solstone/apps/observer/processing_proof.py:71-76; Rust as_u64 rejects floats, yielding the same verdict" + }, + { + "name": "input_size_negative_is_refused", + "expected_handler": "transcribe", + "input_size": 5, + "record": {"schema": "solstone.processing.v1", "state": "analyzed", "handler": "transcribe", "input_size": -5}, + "expected_verdict": "refused", + "provenance_tag": "synthetic", + "citation": "solstone/apps/observer/processing_proof.py:71-76; Rust as_u64 rejects negative integers, yielding the same verdict" + } + ], + "failure_exhaustion": [ + { + "name": "corrupt_input_is_immediately_exhausted", + "record": {"state": "failed", "reason_code": "corrupt_input", "handler": "transcribe", "attempts": 1}, + "expected_exhausted": true, + "provenance_tag": "constructed", + "citation": "solstone/observe/processing_record.py:48-54" + }, + { + "name": "analysis_failed_attempt_one", + "record": {"state": "failed", "reason_code": "analysis_failed", "handler": "describe", "attempts": 1}, + "expected_exhausted": false, + "provenance_tag": "constructed", + "citation": "solstone/observe/processing_record.py:48-64" + }, + { + "name": "analysis_failed_attempt_two", + "record": {"state": "failed", "reason_code": "analysis_failed", "handler": "describe", "attempts": 2}, + "expected_exhausted": false, + "provenance_tag": "constructed", + "citation": "solstone/observe/processing_record.py:48-64" + }, + { + "name": "analysis_failed_attempt_three", + "record": {"state": "failed", "reason_code": "analysis_failed", "handler": "describe", "attempts": 3}, + "expected_exhausted": true, + "provenance_tag": "constructed", + "citation": "solstone/observe/processing_record.py:48-64" + }, + { + "name": "analysis_failed_attempt_four", + "record": {"state": "failed", "reason_code": "analysis_failed", "handler": "describe", "attempts": 4}, + "expected_exhausted": true, + "provenance_tag": "constructed", + "citation": "solstone/observe/processing_record.py:48-64" + }, + { + "name": "attempts_absent", + "record": {"state": "failed", "reason_code": "analysis_failed", "handler": "describe"}, + "expected_exhausted": false, + "provenance_tag": "constructed", + "citation": "solstone/observe/processing_record.py:57-64" + }, + { + "name": "attempts_null", + "record": {"state": "failed", "reason_code": "analysis_failed", "handler": "describe", "attempts": null}, + "expected_exhausted": false, + "provenance_tag": "synthetic", + "citation": "solstone/observe/processing_record.py:57-64" + }, + { + "name": "attempts_string", + "record": {"state": "failed", "reason_code": "analysis_failed", "handler": "describe", "attempts": "3"}, + "expected_exhausted": false, + "provenance_tag": "synthetic", + "citation": "solstone/observe/processing_record.py:57-64" + }, + { + "name": "attempts_bool", + "record": {"state": "failed", "reason_code": "analysis_failed", "handler": "describe", "attempts": true}, + "expected_exhausted": false, + "provenance_tag": "synthetic", + "citation": "solstone/observe/processing_record.py:57-64" + }, + { + "name": "attempts_negative_one_is_not_exhausted", + "record": {"state": "failed", "reason_code": "analysis_failed", "handler": "describe", "attempts": -1}, + "expected_exhausted": false, + "provenance_tag": "synthetic", + "citation": "solstone/observe/processing_record.py:57-64; record_attempts preserves -1 verbatim while this boolean verdict remains false" + }, + { + "name": "nonfailed_corrupt_input_is_not_exhausted", + "record": {"state": "analyzed", "reason_code": "corrupt_input", "handler": "transcribe", "attempts": 1}, + "expected_exhausted": false, + "provenance_tag": "constructed", + "citation": "solstone/observe/processing_record.py:48-54" + } + ] +}