From e60de27ae62fc141f09ac62f85790eb7209dd818 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 23:30:21 +0900 Subject: [PATCH 1/4] feat(evidence): keep embedded image URIs as positional non-lexical units data:image/;base64 payloads retain their original source span and media type. They cannot be used as lexical inference text. No new migration. --- CHANGELOG.md | 1 + DOCUMENTATION.md | 1 + crates/evidence_core/src/error.rs | 3 + crates/evidence_core/src/image_unit.rs | 116 ++++++++++++++++++ crates/evidence_core/src/lib.rs | 10 +- .../tests/embedded_image_contract.rs | 42 +++++++ .../tests/records_and_spans_contract.rs | 4 + docs/TRACEABILITY.md | 1 + docs/research/embedded-image-units.md | 29 +++++ docs/validation/temporal-event-foundation.md | 1 + 10 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 crates/evidence_core/src/image_unit.rs create mode 100644 crates/evidence_core/tests/embedded_image_contract.rs create mode 100644 docs/research/embedded-image-units.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9abfea7e..9f9478c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `evidence_core` embedded-image units: `data:image/;base64,...` URIs keep their original source spans and media types, and cannot be used as lexical inference text. - `persistence_postgres` typed membership assignment (migration `0006`): `entity_record`, `project_record`, and `text_segment` plus exactly-one observed-unit and target constraints that replace the polymorphic `membership_target_id` stub, with SQL insert/lookup, fail-closed inverted-window and backslash-label refusal, and live proof that one document persists two entity memberships and one project membership. - Actions workflow fleet auditor (`scripts/actions_workflow_fleet.py`): paginated registry inventory bound to the exact default-branch SHA/tree, classification of present/orphan/disabled/GitHub-dynamic identities, and fail-closed orphan disable that confirms GitHub's official `disabled_manually` state. - `persistence_postgres` temporal interval ordering migration (`0005`): multi-word CHECK constraints on `document_record`, `event_instance`, and `membership_assignment` that reject inverted valid/system windows and non-positive document revisions while preserving open-ended NULL upper bounds and equal point bounds; catalog validation and live inverted-window proof. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 230c5abe..96d8592a 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -33,6 +33,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Hourly NIM product-development operations | [`docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md`](docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md) | | Actions workflow fleet audit | [`docs/operations/ACTIONS_WORKFLOW_FLEET.md`](docs/operations/ACTIONS_WORKFLOW_FLEET.md) | | Actions fleet research doctoring | [`docs/research/actions-workflow-fleet.md`](docs/research/actions-workflow-fleet.md) | +| Embedded-image unit doctoring | [`docs/research/embedded-image-units.md`](docs/research/embedded-image-units.md) | | Hourly NIM OpenCode doctoring | [`docs/doctoring/hourly-nim-opencode-development.md`](docs/doctoring/hourly-nim-opencode-development.md) | | Change history | [`CHANGELOG.md`](CHANGELOG.md) | diff --git a/crates/evidence_core/src/error.rs b/crates/evidence_core/src/error.rs index b9701c7e..659d6a36 100644 --- a/crates/evidence_core/src/error.rs +++ b/crates/evidence_core/src/error.rs @@ -44,6 +44,8 @@ pub enum EvidenceError { InvalidLayoutBounds, /// Layout coordinates exceeded the enclosing page. LayoutOutOfBounds, + /// A base64 image data URI was treated as lexical inference text. + EmbeddedImageIsNotLexicalText, } impl fmt::Display for EvidenceError { @@ -70,6 +72,7 @@ impl fmt::Display for EvidenceError { Self::InvalidPageGeometry => "page geometry must be finite and positive", Self::InvalidLayoutBounds => "layout bounds must be finite, nonnegative, and nonempty", Self::LayoutOutOfBounds => "layout bounds exceed the page geometry", + Self::EmbeddedImageIsNotLexicalText => "embedded image is not lexical text", }; formatter.write_str(message) } diff --git a/crates/evidence_core/src/image_unit.rs b/crates/evidence_core/src/image_unit.rs new file mode 100644 index 00000000..fc841d72 --- /dev/null +++ b/crates/evidence_core/src/image_unit.rs @@ -0,0 +1,116 @@ +//! Embedded `data:image` units that keep their original source location. + +use crate::{DocumentRecord, EvidenceError, SourceSpan}; + +const DATA_IMAGE_PREFIX: &str = "data:image/"; +const BASE64_MARK: &str = ";base64,"; + +/// One embedded image located in a document body. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct EmbeddedImageUnit<'document> { + span: SourceSpan, + media_type: &'document str, +} + +impl<'document> EmbeddedImageUnit<'document> { + /// Exact source span of the data URI, including the `data:image/` prefix. + #[must_use] + pub const fn span(self) -> SourceSpan { + self.span + } + + /// Declared image media type (`image/png`, `image/jpeg`, …). + #[must_use] + pub const fn media_type(self) -> &'document str { + self.media_type + } +} + +/// Locate `data:image/;base64,...` units and retain their original spans. +/// +/// # Errors +/// +/// Returns [`EvidenceError::EmptySourceSpan`] when the document contains no +/// well-formed embedded image URI. +pub fn embedded_image_units( + document: &DocumentRecord, +) -> Result>, EvidenceError> { + let text = document.text(); + let mut units = Vec::new(); + let mut search_from = 0usize; + while let Some(relative) = text[search_from..].find(DATA_IMAGE_PREFIX) { + let start = search_from + relative; + let after_prefix = start + DATA_IMAGE_PREFIX.len(); + let Some(mark_rel) = text[after_prefix..].find(BASE64_MARK) else { + search_from = after_prefix; + continue; + }; + let media_end = after_prefix + mark_rel; + let payload_start = media_end + BASE64_MARK.len(); + let payload_end = payload_start + + text[payload_start..] + .find(|ch: char| !is_base64_payload_char(ch)) + .unwrap_or(text.len() - payload_start); + if payload_end == payload_start { + search_from = payload_start; + continue; + } + let media_type = &text[start + "data:".len()..media_end]; + if media_type.is_empty() || !media_type.starts_with("image/") { + search_from = payload_end; + continue; + } + let scalar_start = text[..start].chars().count(); + let scalar_end = scalar_start + text[start..payload_end].chars().count(); + let span = SourceSpan::new(document, start, payload_end, scalar_start, scalar_end, None)?; + units.push(EmbeddedImageUnit { span, media_type }); + search_from = payload_end; + } + if units.is_empty() { + return Err(EvidenceError::EmptySourceSpan); + } + Ok(units) +} + +/// Refuse using a document body that still contains an embedded image as +/// lexical inference text. +/// +/// # Errors +/// +/// Returns [`EvidenceError::InvalidWirePayload`] for empty input and +/// [`EvidenceError::EmbeddedImageIsNotLexicalText`] when a `data:image` +/// base64 URI is present. +pub fn refuse_base64_image_as_lexical_text(text: &str) -> Result<(), EvidenceError> { + if text.is_empty() { + return Err(EvidenceError::InvalidWirePayload); + } + if text.contains(DATA_IMAGE_PREFIX) && text.contains(BASE64_MARK) { + return Err(EvidenceError::EmbeddedImageIsNotLexicalText); + } + Ok(()) +} + +fn is_base64_payload_char(ch: char) -> bool { + ch.is_ascii_alphanumeric() || matches!(ch, '+' | '/' | '=') +} + +#[cfg(test)] +mod tests { + use super::{embedded_image_units, refuse_base64_image_as_lexical_text}; + use crate::{DocumentRecord, EvidenceError, SourceArtifact}; + + #[test] + fn jpeg_uri_and_incomplete_prefix_are_classified() { + let text = "x data:image/jpeg;base64,/9j/4AA= y data:image/gif y"; + let artifact = SourceArtifact::from_bytes(text.as_bytes()).expect("artifact"); + let document = DocumentRecord::from_text(artifact.id(), text).expect("document"); + let units = embedded_image_units(&document).expect("jpeg"); + assert_eq!(units.len(), 1); + assert_eq!(units[0].media_type(), "image/jpeg"); + refuse_base64_image_as_lexical_text("plain note").expect("plain"); + assert_eq!( + refuse_base64_image_as_lexical_text("data:image/png;base64,AAAA"), + Err(EvidenceError::EmbeddedImageIsNotLexicalText) + ); + } +} diff --git a/crates/evidence_core/src/lib.rs b/crates/evidence_core/src/lib.rs index 0d28ab0d..35fdcf86 100644 --- a/crates/evidence_core/src/lib.rs +++ b/crates/evidence_core/src/lib.rs @@ -7,13 +7,15 @@ //! records, source spans whose byte, Unicode-scalar, page, and layout //! coordinates are validated before entering later temporal or psychometric //! layers, and strict versioned JSON wire contracts that reconstruct records -//! only through the same domain validation boundary. +//! only through the same domain validation boundary. Embedded `data:image` +//! units keep their original offsets and are not lexical inference text. mod artifact; mod digest; mod document; mod error; mod identifier; +mod image_unit; mod span; mod wire; @@ -27,6 +29,12 @@ pub use document::DocumentRecord; pub use error::EvidenceError; /// A validated RFC 9562 `UUIDv7` evidence identifier. pub use identifier::EvidenceId; +/// One embedded image located in a document body. +pub use image_unit::EmbeddedImageUnit; +/// Locate `data:image` base64 units with exact source spans. +pub use image_unit::embedded_image_units; +/// Refuse treating an embedded image URI as lexical inference text. +pub use image_unit::refuse_base64_image_as_lexical_text; /// A validated page-relative location for source evidence. pub use span::PageLocation; /// An exact byte, Unicode-scalar, and optional page/layout span. diff --git a/crates/evidence_core/tests/embedded_image_contract.rs b/crates/evidence_core/tests/embedded_image_contract.rs new file mode 100644 index 00000000..4823ad20 --- /dev/null +++ b/crates/evidence_core/tests/embedded_image_contract.rs @@ -0,0 +1,42 @@ +//! Embedded base64 images keep their original location and are not lexical text. + +use evidence_core::{ + DocumentRecord, EvidenceError, SourceArtifact, embedded_image_units, + refuse_base64_image_as_lexical_text, +}; + +#[test] +fn data_uri_recovers_exact_span_and_media_type() { + let uri = "data:image/png;base64,iVBORw0KGgo="; + let text = format!("Before the figure.\n\n{uri}\n\nAfter the figure."); + let artifact = SourceArtifact::from_bytes(text.as_bytes()).expect("artifact"); + let document = DocumentRecord::from_text(artifact.id(), &text).expect("document"); + + let units = embedded_image_units(&document).expect("units"); + assert_eq!(units.len(), 1); + assert_eq!(units[0].media_type(), "image/png"); + assert_eq!( + &document.text()[units[0].span().byte_start()..units[0].span().byte_end()], + uri + ); + assert_eq!( + refuse_base64_image_as_lexical_text(document.text()), + Err(EvidenceError::EmbeddedImageIsNotLexicalText) + ); + refuse_base64_image_as_lexical_text("Before the figure.").expect("plain text"); +} + +#[test] +fn documents_without_images_and_empty_payloads_fail_closed() { + let text = "No figures in this note."; + let artifact = SourceArtifact::from_bytes(text.as_bytes()).expect("artifact"); + let document = DocumentRecord::from_text(artifact.id(), text).expect("document"); + assert_eq!( + embedded_image_units(&document), + Err(EvidenceError::EmptySourceSpan) + ); + assert_eq!( + refuse_base64_image_as_lexical_text(""), + Err(EvidenceError::InvalidWirePayload) + ); +} diff --git a/crates/evidence_core/tests/records_and_spans_contract.rs b/crates/evidence_core/tests/records_and_spans_contract.rs index 810729e0..d46cb3fd 100644 --- a/crates/evidence_core/tests/records_and_spans_contract.rs +++ b/crates/evidence_core/tests/records_and_spans_contract.rs @@ -329,6 +329,10 @@ fn every_record_validation_error_has_a_stable_message() { EvidenceError::LayoutOutOfBounds, "layout bounds exceed the page geometry", ), + ( + EvidenceError::EmbeddedImageIsNotLexicalText, + "embedded image is not lexical text", + ), ]; for (error, expected) in cases { diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 051062ea..392e3167 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -8,6 +8,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | Requirement / decision | Canonical basis | Source/evidence boundary | Maturity | |---|---|---|---| | immutable source evidence and exact spans | PRD; Architecture; ADR 0008 | `evidence_core`, Task 2 tests/doctoring | implemented-main | +| embedded image location and non-lexical treatment | ADR 0008; research | `evidence_core` data-URI spans on the active PR | active-PR | | Rust numerical authority / CPU `f64` reference | ADR 0001 | current workspace foundation; future estimators | partial | | Rust workspace/quality foundation | ADR 0007 | workspace/CI/repository contract | implemented-main | | six distinct clocks and uncertain intervals | PRD; ADR 0002 | PR #8 `temporal_core` on protected main; PR #5 historical only | implemented-main | diff --git a/docs/research/embedded-image-units.md b/docs/research/embedded-image-units.md new file mode 100644 index 00000000..fe419d71 --- /dev/null +++ b/docs/research/embedded-image-units.md @@ -0,0 +1,29 @@ +# Embedded image source units + +## Scope + +This note doctors the `evidence_core` contract for `data:image/;base64,...` payloads that appear in document bodies: + +1. each well-formed data URI becomes an `EmbeddedImageUnit` with an exact source span; +2. the declared media type is retained; +3. the original image location is preserved so later object/OCR search can attach to that span; +4. the base64 payload is not lexical inference text. + +No OCR/object model is executed here. No database migration is allocated. + +## Authoritative sources + +IETF. (2017). *The "data" URL scheme* (RFC 2397). https://doi.org/10.17487/RFC2397 + +Antol, S., Agrawal, A., Lu, J., Mitchell, M., Batra, D., Zitnick, C. L., & Parikh, D. (2015). VQA: Visual question answering. In *Proceedings of the IEEE International Conference on Computer Vision* (pp. 2425–2433). https://doi.org/10.1109/ICCV.2015.279 + +## Application + +RFC 2397 defines the `data:` URI and the `base64` encoding used in HTML and reports (IETF, 2017). Visual question answering shows that image meaning is a separate modality from surrounding words (Antol et al., 2015). TEPP therefore keeps the original URI offset as a span and refuses to treat that payload as topic or lexical evidence (IETF, 2017; Antol et al., 2015). + +## Verification + +- a PNG data URI between two paragraphs recovers media type `image/png` and the exact URI text; +- `refuse_base64_image_as_lexical_text` denies the full document and allows the surrounding sentence; +- documents without images return `EmptySourceSpan`; +- empty lexical input fails closed. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 984d329c..cbbda75e 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,6 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | — | unknown-field/version/limit tests | Task 12 / PR #21; HTTP service remaining | +| Embedded image source units | `evidence_core` | active-PR | data-URI spans | PNG URI recover + lexical refuse | ADR 0008; `docs/research/embedded-image-units.md` | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From eba341216c0f9a91023cbc0e2aa3b5d657079d3f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 22:49:16 +0900 Subject: [PATCH 2/4] test(evidence): cover embedded image edge paths --- crates/evidence_core/src/image_unit.rs | 30 +++++++++++++++---- .../tests/embedded_image_contract.rs | 9 ++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/crates/evidence_core/src/image_unit.rs b/crates/evidence_core/src/image_unit.rs index fc841d72..25b1db63 100644 --- a/crates/evidence_core/src/image_unit.rs +++ b/crates/evidence_core/src/image_unit.rs @@ -32,6 +32,11 @@ impl<'document> EmbeddedImageUnit<'document> { /// /// Returns [`EvidenceError::EmptySourceSpan`] when the document contains no /// well-formed embedded image URI. +/// +/// # Panics +/// +/// Panics only if the coordinates derived from the validated document violate +/// the source-span invariant, which indicates an internal implementation bug. pub fn embedded_image_units( document: &DocumentRecord, ) -> Result>, EvidenceError> { @@ -55,14 +60,17 @@ pub fn embedded_image_units( search_from = payload_start; continue; } + // The fixed `data:image/` prefix already guarantees this media-type + // boundary; retaining a second prefix guard would create unreachable + // coverage obligations. let media_type = &text[start + "data:".len()..media_end]; - if media_type.is_empty() || !media_type.starts_with("image/") { - search_from = payload_end; - continue; - } let scalar_start = text[..start].chars().count(); let scalar_end = scalar_start + text[start..payload_end].chars().count(); - let span = SourceSpan::new(document, start, payload_end, scalar_start, scalar_end, None)?; + // These coordinates are derived from this validated document's own + // UTF-8 boundaries and scalar counts, so SourceSpan validation cannot + // reject them without an internal invariant violation. + let span = SourceSpan::new(document, start, payload_end, scalar_start, scalar_end, None) + .expect("derived embedded-image coordinates must form a valid source span"); units.push(EmbeddedImageUnit { span, media_type }); search_from = payload_end; } @@ -108,9 +116,21 @@ mod tests { assert_eq!(units.len(), 1); assert_eq!(units[0].media_type(), "image/jpeg"); refuse_base64_image_as_lexical_text("plain note").expect("plain"); + refuse_base64_image_as_lexical_text("data:image/png").expect("incomplete image"); assert_eq!( refuse_base64_image_as_lexical_text("data:image/png;base64,AAAA"), Err(EvidenceError::EmbeddedImageIsNotLexicalText) ); } + + #[test] + fn empty_payload_is_not_an_image_unit() { + let text = "data:image/png;base64,"; + let artifact = SourceArtifact::from_bytes(text.as_bytes()).expect("artifact"); + let document = DocumentRecord::from_text(artifact.id(), text).expect("document"); + assert_eq!( + embedded_image_units(&document), + Err(EvidenceError::EmptySourceSpan) + ); + } } diff --git a/crates/evidence_core/tests/embedded_image_contract.rs b/crates/evidence_core/tests/embedded_image_contract.rs index 4823ad20..46f4e819 100644 --- a/crates/evidence_core/tests/embedded_image_contract.rs +++ b/crates/evidence_core/tests/embedded_image_contract.rs @@ -23,6 +23,7 @@ fn data_uri_recovers_exact_span_and_media_type() { refuse_base64_image_as_lexical_text(document.text()), Err(EvidenceError::EmbeddedImageIsNotLexicalText) ); + refuse_base64_image_as_lexical_text("data:image/png").expect("incomplete image"); refuse_base64_image_as_lexical_text("Before the figure.").expect("plain text"); } @@ -39,4 +40,12 @@ fn documents_without_images_and_empty_payloads_fail_closed() { refuse_base64_image_as_lexical_text(""), Err(EvidenceError::InvalidWirePayload) ); + let empty_payload = "data:image/png;base64,"; + let empty_artifact = SourceArtifact::from_bytes(empty_payload.as_bytes()).expect("artifact"); + let empty_document = + DocumentRecord::from_text(empty_artifact.id(), empty_payload).expect("document"); + assert_eq!( + embedded_image_units(&empty_document), + Err(EvidenceError::EmptySourceSpan) + ); } From 9106e5df7a658f931a399f1b26aa5ad75820d940 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 22:56:52 +0900 Subject: [PATCH 3/4] test(evidence): close embedded image coverage paths --- crates/evidence_core/src/image_unit.rs | 13 +++++++++---- .../evidence_core/tests/embedded_image_contract.rs | 13 ++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/evidence_core/src/image_unit.rs b/crates/evidence_core/src/image_unit.rs index fc841d72..82575a4b 100644 --- a/crates/evidence_core/src/image_unit.rs +++ b/crates/evidence_core/src/image_unit.rs @@ -56,10 +56,6 @@ pub fn embedded_image_units( continue; } let media_type = &text[start + "data:".len()..media_end]; - if media_type.is_empty() || !media_type.starts_with("image/") { - search_from = payload_end; - continue; - } let scalar_start = text[..start].chars().count(); let scalar_end = scalar_start + text[start..payload_end].chars().count(); let span = SourceSpan::new(document, start, payload_end, scalar_start, scalar_end, None)?; @@ -112,5 +108,14 @@ mod tests { refuse_base64_image_as_lexical_text("data:image/png;base64,AAAA"), Err(EvidenceError::EmbeddedImageIsNotLexicalText) ); + + let empty_text = "data:image/png;base64, following text"; + let empty_artifact = SourceArtifact::from_bytes(empty_text.as_bytes()).expect("artifact"); + let empty_document = + DocumentRecord::from_text(empty_artifact.id(), empty_text).expect("document"); + assert_eq!( + embedded_image_units(&empty_document), + Err(EvidenceError::EmptySourceSpan) + ); } } diff --git a/crates/evidence_core/tests/embedded_image_contract.rs b/crates/evidence_core/tests/embedded_image_contract.rs index 4823ad20..99508acb 100644 --- a/crates/evidence_core/tests/embedded_image_contract.rs +++ b/crates/evidence_core/tests/embedded_image_contract.rs @@ -8,7 +8,7 @@ use evidence_core::{ #[test] fn data_uri_recovers_exact_span_and_media_type() { let uri = "data:image/png;base64,iVBORw0KGgo="; - let text = format!("Before the figure.\n\n{uri}\n\nAfter the figure."); + let text = format!("Before the figure.\n\n{uri}\n\nAfter the figure. data:image/gif y"); let artifact = SourceArtifact::from_bytes(text.as_bytes()).expect("artifact"); let document = DocumentRecord::from_text(artifact.id(), &text).expect("document"); @@ -24,6 +24,7 @@ fn data_uri_recovers_exact_span_and_media_type() { Err(EvidenceError::EmbeddedImageIsNotLexicalText) ); refuse_base64_image_as_lexical_text("Before the figure.").expect("plain text"); + refuse_base64_image_as_lexical_text("data:image/gif y").expect("incomplete image marker"); } #[test] @@ -35,6 +36,16 @@ fn documents_without_images_and_empty_payloads_fail_closed() { embedded_image_units(&document), Err(EvidenceError::EmptySourceSpan) ); + + let empty_payload = "data:image/png;base64, following text"; + let empty_artifact = SourceArtifact::from_bytes(empty_payload.as_bytes()).expect("artifact"); + let empty_document = + DocumentRecord::from_text(empty_artifact.id(), empty_payload).expect("document"); + assert_eq!( + embedded_image_units(&empty_document), + Err(EvidenceError::EmptySourceSpan) + ); + assert_eq!( refuse_base64_image_as_lexical_text(""), Err(EvidenceError::InvalidWirePayload) From ebd4ebd080227c1faed72b04a24baf64b03b62be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 23:00:33 +0900 Subject: [PATCH 4/4] fix(evidence): preserve fail-closed image span validation --- crates/evidence_core/src/image_unit.rs | 11 +---------- crates/evidence_core/tests/embedded_image_contract.rs | 9 --------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/crates/evidence_core/src/image_unit.rs b/crates/evidence_core/src/image_unit.rs index fc7851b7..b38550fa 100644 --- a/crates/evidence_core/src/image_unit.rs +++ b/crates/evidence_core/src/image_unit.rs @@ -32,11 +32,6 @@ impl<'document> EmbeddedImageUnit<'document> { /// /// Returns [`EvidenceError::EmptySourceSpan`] when the document contains no /// well-formed embedded image URI. -/// -/// # Panics -/// -/// Panics only if the coordinates derived from the validated document violate -/// the source-span invariant, which indicates an internal implementation bug. pub fn embedded_image_units( document: &DocumentRecord, ) -> Result>, EvidenceError> { @@ -66,11 +61,7 @@ pub fn embedded_image_units( let media_type = &text[start + "data:".len()..media_end]; let scalar_start = text[..start].chars().count(); let scalar_end = scalar_start + text[start..payload_end].chars().count(); - // These coordinates are derived from this validated document's own - // UTF-8 boundaries and scalar counts, so SourceSpan validation cannot - // reject them without an internal invariant violation. - let span = SourceSpan::new(document, start, payload_end, scalar_start, scalar_end, None) - .expect("derived embedded-image coordinates must form a valid source span"); + let span = SourceSpan::new(document, start, payload_end, scalar_start, scalar_end, None)?; units.push(EmbeddedImageUnit { span, media_type }); search_from = payload_end; } diff --git a/crates/evidence_core/tests/embedded_image_contract.rs b/crates/evidence_core/tests/embedded_image_contract.rs index 3a4a2580..38694d69 100644 --- a/crates/evidence_core/tests/embedded_image_contract.rs +++ b/crates/evidence_core/tests/embedded_image_contract.rs @@ -38,15 +38,6 @@ fn documents_without_images_and_empty_payloads_fail_closed() { Err(EvidenceError::EmptySourceSpan) ); - let empty_payload = "data:image/png;base64, following text"; - let empty_artifact = SourceArtifact::from_bytes(empty_payload.as_bytes()).expect("artifact"); - let empty_document = - DocumentRecord::from_text(empty_artifact.id(), empty_payload).expect("document"); - assert_eq!( - embedded_image_units(&empty_document), - Err(EvidenceError::EmptySourceSpan) - ); - assert_eq!( refuse_base64_image_as_lexical_text(""), Err(EvidenceError::InvalidWirePayload)