Text Projection: whole-document conformance, and a corpus with teeth
Thirteen drift-locked document vectors -- four accept, nine reject, one per implemented rejection class -- gated as conformance step [7e] beside [7d]'s 65 decode vectors. The checker distinguishes three outcomes, and that distinction is the whole point. `Rejected`, `AcceptedCanonical`, and `AcceptedNonCanonical` are separate verdicts, and every one of the four wrong pairings is a failure: an accept vector that merely normalizes fails, and a reject vector that is accepted -- whether as canonical or after normalization -- fails. The decode corpus learned this the expensive way, where collapsing "rejected" with "accepted but normalized" let a corpus pass against the exact decoders it was written to catch. Accept vectors assert the byte-checkable equation `project(serialize(parse(T))) == T`, which quantifies over TEXTS. The comments say plainly that it must not be replaced with bundle identity, and name the three deliberate binary losses -- duplicate blobs collapsing under derived-ordering, regenerated physical layout, and dropped accelerators -- so the next reader does not "fix" it into a bundle comparison and then chase a phantom. Reach is asserted exactly, not merely non-zero: two documents each carrying an extension, a canonical base, a custom profile, a lineage, and more than one envelope; and every reject class must have exactly one vector the parser really refused. A mislabelled reject drops its class to zero rather than collecting credit from its label. The corpus was verified by breaking the implementation, not the checker: making `parse_document` silently tolerate a missing trailing LF is caught three ways, and the suite reports "declared reject, but was ACCEPTED and normalized". Review fixed one defect in the semantic harness. It claimed to check `semantics(parse(project(B))) == semantics(B)` but compared `parse(project(B))` against `document_from_bundle(B)` -- and `project_bundle` *is* `project_text_document(document_from_bundle(..))`, so both sides flowed through one function and any bug in it cancelled out. Dropping an envelope there passed. It now compares against the envelopes the test itself serialized in, which is independent of the projection path; the same mutation fails. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
244bd6bd96
commit
a9a57120e9
|
|
@ -1284,6 +1284,7 @@ dependencies = [
|
|||
"epiphany-engrave",
|
||||
"epiphany-layout-ir",
|
||||
"epiphany-ops",
|
||||
"epiphany-textproj",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ epiphany-determinism.workspace = true
|
|||
epiphany-core.workspace = true
|
||||
epiphany-bundle.workspace = true
|
||||
epiphany-ops.workspace = true
|
||||
epiphany-textproj.workspace = true
|
||||
# Agent E's layout IR has landed; the layout round-trip harness (criterion 6)
|
||||
# now drives the real crate instead of an in-tree stub.
|
||||
epiphany-layout-ir.workspace = true
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
use epiphany_testkit::{
|
||||
bundle_harness, convergence, corpus, editloop, equivocation, fixtures, generators, layout_stub,
|
||||
negative, prepass_harness, roundtrip, Rng,
|
||||
negative, prepass_harness, roundtrip, textproj, Rng,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
|
|
@ -173,5 +173,31 @@ fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
// 7e. Whole-document Text Projection vectors. The strong equation is over
|
||||
// text bytes, never bundle identity; the generated-operation pass also
|
||||
// checks the weaker semantic equation by reducing both sides.
|
||||
eprintln!("[7e ] Text Projection whole-document conformance vectors");
|
||||
for seed in 0..n(16) {
|
||||
textproj::assert_semantics_preserved(seed);
|
||||
}
|
||||
{
|
||||
use epiphany_textproj::vectors;
|
||||
assert_eq!(
|
||||
vectors::COMMITTED,
|
||||
vectors::render(),
|
||||
"{} is stale; regenerate with `cargo run -q -p epiphany-testkit \
|
||||
--example generate_vectors`",
|
||||
vectors::PATH
|
||||
);
|
||||
match vectors::verify(vectors::COMMITTED) {
|
||||
Ok(n) => eprintln!(" {n} vectors, every verdict agreed"),
|
||||
Err(failures) => panic!(
|
||||
"{} Text Projection vector disagreement(s):\n{}",
|
||||
failures.len(),
|
||||
failures.join("\n")
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("[8/8] ok: full conformance suite passed (scale {scale})");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
//! Regenerates `spec/vectors/decode_vectors.txt`, the cross-implementation
|
||||
//! decode conformance corpus. Run from the workspace root.
|
||||
//! Regenerates both committed cross-implementation conformance corpora. Run
|
||||
//! from the workspace root.
|
||||
fn main() {
|
||||
let text = epiphany_testkit::vectors::render();
|
||||
let path = epiphany_testkit::vectors::PATH;
|
||||
std::fs::write(path, &text).unwrap_or_else(|e| panic!("writing {path}: {e}"));
|
||||
let rows = epiphany_testkit::vectors::parse(&text).expect("parses");
|
||||
eprintln!("wrote {} vectors to {path}", rows.len());
|
||||
|
||||
let text = epiphany_textproj::vectors::render();
|
||||
let path = epiphany_textproj::vectors::PATH;
|
||||
std::fs::write(path, &text).unwrap_or_else(|e| panic!("writing {path}: {e}"));
|
||||
let rows = epiphany_textproj::vectors::parse(&text).expect("parses");
|
||||
eprintln!("wrote {} vectors to {path}", rows.len());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@
|
|||
//! materialization through a bundle snapshot.
|
||||
|
||||
pub mod rng;
|
||||
pub mod textproj;
|
||||
pub mod vectors;
|
||||
|
||||
// Phase 2, Agent F (worklist F1): the Chapter 10 performance-budget gate the
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
//! End-to-end semantic preservation for the Text Projection document layer.
|
||||
//!
|
||||
//! This is the weaker `req:textproj:roundtrip` equation over generated real
|
||||
//! operations:
|
||||
//!
|
||||
//! `semantics(parse(project(B))) == semantics(B)`.
|
||||
//!
|
||||
//! It compares canonical reducer bytes, never bundle bytes. A text round trip
|
||||
//! intentionally regenerates layout, collapses duplicate blobs, and omits
|
||||
//! non-canonical accelerators; none of those changes operation semantics.
|
||||
|
||||
use epiphany_bundle::{DocumentId, FileUuid, MemStore, ProfileDeclaration};
|
||||
use epiphany_ops::{OperationEnvelope, OperationSet};
|
||||
use epiphany_textproj::parse::parse_document;
|
||||
use epiphany_textproj::project::{document_from_bundle, project_bundle};
|
||||
use epiphany_textproj::serialize::serialize_document;
|
||||
use epiphany_textproj::TextDocument;
|
||||
|
||||
use crate::{generators, Rng};
|
||||
|
||||
fn reduced_bytes(envelopes: &[OperationEnvelope]) -> Vec<u8> {
|
||||
let mut operations = OperationSet::new();
|
||||
operations.accept_all(envelopes.iter().cloned());
|
||||
operations.reduce().canonical_bytes()
|
||||
}
|
||||
|
||||
/// Generates a bundle carrying real operations and checks the semantic
|
||||
/// projection equation by reducing both envelope sets to canonical bytes.
|
||||
pub fn assert_semantics_preserved(seed: u64) {
|
||||
let mut rng = Rng::new(seed);
|
||||
let envelopes = generators::operation_envelopes(&mut rng, 24, 3, 8, 8);
|
||||
assert!(
|
||||
!envelopes.is_empty(),
|
||||
"the generated bundle carries operations"
|
||||
);
|
||||
|
||||
let source = TextDocument {
|
||||
document_id: DocumentId(seed.to_le_bytes().repeat(2).try_into().expect("16 bytes")),
|
||||
lineage_id: None,
|
||||
profiles: vec![ProfileDeclaration::full()],
|
||||
extensions: Vec::new(),
|
||||
canonical_base: None,
|
||||
blobs: Vec::new(),
|
||||
envelopes,
|
||||
};
|
||||
let bundle = serialize_document(&source, MemStore::new(), FileUuid([0x7E; 16]))
|
||||
.unwrap_or_else(|error| panic!("seed {seed}: serializing generated operations: {error}"));
|
||||
|
||||
let bundle_document = document_from_bundle(&bundle)
|
||||
.unwrap_or_else(|error| panic!("seed {seed}: reading source bundle: {error}"));
|
||||
assert!(
|
||||
!bundle_document.envelopes.is_empty(),
|
||||
"seed {seed}: source bundle lost every operation"
|
||||
);
|
||||
let text = project_bundle(&bundle)
|
||||
.unwrap_or_else(|error| panic!("seed {seed}: projecting source bundle: {error}"));
|
||||
let parsed = parse_document(&text)
|
||||
.unwrap_or_else(|error| panic!("seed {seed}: parsing its projection: {error}"));
|
||||
|
||||
// `semantics(B)` must be computed WITHOUT going through the projection.
|
||||
// Comparing against `bundle_document` alone would not: `project_bundle` is
|
||||
// `project_text_document(document_from_bundle(..))`, so both sides would flow
|
||||
// through `document_from_bundle` and any bug in it — dropping an envelope,
|
||||
// reordering, corrupting one — would cancel out. `source.envelopes` is the
|
||||
// independent reference: this test built it and serialized it in, and
|
||||
// `reduced_bytes` reduces through an `OperationSet`, which imposes canonical
|
||||
// order itself, so the input order does not matter.
|
||||
assert_eq!(
|
||||
reduced_bytes(&parsed.envelopes),
|
||||
reduced_bytes(&source.envelopes),
|
||||
"seed {seed}: semantics(parse(project(B))) != semantics(B)"
|
||||
);
|
||||
// And the read-back path agrees with the same independent reference.
|
||||
assert_eq!(
|
||||
reduced_bytes(&bundle_document.envelopes),
|
||||
reduced_bytes(&source.envelopes),
|
||||
"seed {seed}: reading the bundle back changed its semantics"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generated_operation_bundles_preserve_reduced_semantics_not_bundle_identity() {
|
||||
for seed in 0..16 {
|
||||
assert_semantics_preserved(seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
pub mod parse;
|
||||
pub mod project;
|
||||
pub mod serialize;
|
||||
pub mod vectors;
|
||||
|
||||
use epiphany_bundle::{
|
||||
ChunkKind, DocumentId, ExtensionId, FrontierBytes, LineageId, ProfileDeclaration, ProfileId,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,686 @@
|
|||
//! Whole-document Text Projection conformance vectors.
|
||||
//!
|
||||
//! The committed corpus is generated here, inside `epiphany-textproj`, because
|
||||
//! malformed documents must be constructed without going through the public
|
||||
//! projection API that canonicalizes outbound data. Each row carries a complete
|
||||
//! UTF-8 document and a normative accept/reject verdict.
|
||||
//!
|
||||
//! The accept contract is the byte-checkable, text-quantified equation
|
||||
//!
|
||||
//! `project(serialize(parse(T))) == T`.
|
||||
//!
|
||||
//! It is deliberately not bundle identity. Bundle -> text -> bundle collapses
|
||||
//! duplicate blobs, regenerates physical layout (offsets, compression, and
|
||||
//! chunking), and drops non-canonical accelerators. Those erasures preserve
|
||||
//! semantics but make a bundle-byte comparison both false and misleading.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use epiphany_bundle::{
|
||||
ChunkKind, DocumentId, ExtensionId, FileUuid, FrontierBytes, LineageId, MemStore,
|
||||
ProfileConstraints, ProfileDeclaration, ProfileId, ProfileRegistryId,
|
||||
ReductionAlgorithmVersion, SchemaVersion, SemVer, SnapshotId,
|
||||
};
|
||||
use epiphany_core::{OperationId, RegionId, ReplicaId, WallClockTime};
|
||||
use epiphany_ops::{
|
||||
AuthorId, CausalContext, DeleteRegionOp, HybridLogicalClock, OperationEnvelope, OperationKind,
|
||||
OperationPayload, OperationStamp,
|
||||
};
|
||||
|
||||
use crate::parse::parse_document;
|
||||
use crate::project::{project_bundle, project_text_document};
|
||||
use crate::serialize::serialize_document;
|
||||
use crate::{TextCanonicalBase, TextChunk, TextDocument, TextExtension};
|
||||
|
||||
/// The corpus surface owned by this crate.
|
||||
pub const SURFACE: &str = "textproj.document";
|
||||
/// The committed whole-document corpus.
|
||||
pub const COMMITTED: &str = include_str!("../../../spec/vectors/textproj_document_vectors.txt");
|
||||
/// The path regenerated by the vector generator, relative to the workspace root.
|
||||
pub const PATH: &str = "spec/vectors/textproj_document_vectors.txt";
|
||||
|
||||
const HEADER: &str = "\
|
||||
# Epiphany Text Projection document vectors — format version 1
|
||||
#
|
||||
# Generated. Regenerate with:
|
||||
# cargo run -q -p epiphany-testkit --example generate_vectors
|
||||
# `epiphany_textproj::vectors::the_committed_corpus_matches_the_generator` fails
|
||||
# on drift, so a projection change must land here deliberately.
|
||||
#
|
||||
# One vector per line, space-separated:
|
||||
#
|
||||
# <surface> <verdict> <class> <name> <utf8-hex>
|
||||
#
|
||||
# NORMATIVE: Text Projection companion, `req:textproj:roundtrip` and
|
||||
# `req:textproj:strict-parse`. Every `accept` text must satisfy the exact-byte
|
||||
# equation project(serialize(parse(T))) == T. Every `reject` text must be
|
||||
# refused, not accepted and normalized to a different canonical spelling.
|
||||
#
|
||||
# The equation quantifies over TEXTS, not bundle bytes. Binary layout is freely
|
||||
# regenerated, duplicate blobs collapse, and non-canonical accelerators vanish;
|
||||
# all three preserve semantics and none weakens this text identity check.
|
||||
#
|
||||
# `class` names the rejection exercised. It is informative; the verdict and
|
||||
# document bytes are normative. `<utf8-hex>` is lowercase with no separators.
|
||||
";
|
||||
|
||||
/// One generated vector: `(surface, verdict, class, name, UTF-8 bytes)`.
|
||||
pub type TextVector = (
|
||||
&'static str,
|
||||
&'static str,
|
||||
&'static str,
|
||||
&'static str,
|
||||
Vec<u8>,
|
||||
);
|
||||
|
||||
/// The typed outcome of checking one whole document.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CheckVerdict {
|
||||
/// The parser refused the text.
|
||||
Rejected(String),
|
||||
/// The parser accepted it and the byte-checkable equation held exactly.
|
||||
AcceptedCanonical,
|
||||
/// The parser accepted it, but serialization/projection either failed or
|
||||
/// produced different bytes. This is acceptance, never rejection.
|
||||
AcceptedNonCanonical(String),
|
||||
}
|
||||
|
||||
/// One parsed row from the committed text file.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Row {
|
||||
/// Owning conformance surface.
|
||||
pub surface: String,
|
||||
/// Normative `accept` or `reject` verdict.
|
||||
pub verdict: String,
|
||||
/// Informative rejection class, or `-` for accepts.
|
||||
pub class: String,
|
||||
/// Stable vector name.
|
||||
pub name: String,
|
||||
/// Complete UTF-8 document bytes.
|
||||
pub text: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Coverage counts asserted by the corpus gate.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReachCounts {
|
||||
/// Number of accepted documents containing at least one extension.
|
||||
pub extensions: usize,
|
||||
/// Number containing a canonical base.
|
||||
pub canonical_bases: usize,
|
||||
/// Number declaring a custom profile.
|
||||
pub custom_profiles: usize,
|
||||
/// Number carrying a lineage id.
|
||||
pub lineages: usize,
|
||||
/// Number carrying more than one envelope.
|
||||
pub multi_envelope: usize,
|
||||
/// Actually rejected vectors per declared rejection class.
|
||||
pub reject_classes: BTreeMap<String, usize>,
|
||||
}
|
||||
|
||||
/// The exact non-vacuity contract of this corpus: four accepted documents, two
|
||||
/// reaching each optional/rich feature, and one actually rejected vector for
|
||||
/// each of the nine distinct rejection classes implemented by this layer.
|
||||
pub fn expected_reach() -> ReachCounts {
|
||||
ReachCounts {
|
||||
extensions: 2,
|
||||
canonical_bases: 2,
|
||||
custom_profiles: 2,
|
||||
lineages: 2,
|
||||
multi_envelope: 2,
|
||||
reject_classes: [
|
||||
("blob-line", 1),
|
||||
("extension-chunk-order", 1),
|
||||
("extension-declaration-order", 1),
|
||||
("missing-trailing-lf", 1),
|
||||
("operation-envelope-order", 1),
|
||||
("out-of-order-sections", 1),
|
||||
("profile-declaration-order", 1),
|
||||
("repeated-singular-section", 1),
|
||||
("wrong-header-version", 1),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(class, count)| (class.to_owned(), count))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_envelope(counter: u64, physical_time: i64) -> OperationEnvelope {
|
||||
let id = OperationId::new(ReplicaId(1), counter);
|
||||
OperationEnvelope {
|
||||
id,
|
||||
author: AuthorId(0xAB),
|
||||
stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical_time), 0), id),
|
||||
causal_context: CausalContext::new(),
|
||||
transaction: None,
|
||||
payload: OperationPayload::Primitive(OperationKind::DeleteRegion(DeleteRegionOp {
|
||||
region: RegionId::new(ReplicaId(1), counter),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn profiles(custom: bool) -> Vec<ProfileDeclaration> {
|
||||
let mut profiles = vec![ProfileDeclaration::full()];
|
||||
if custom {
|
||||
profiles.push(ProfileDeclaration {
|
||||
profile_id: ProfileId::Custom(ProfileRegistryId([0xCC; 16])),
|
||||
version: SemVer::new(1, 2, 3),
|
||||
constraints: ProfileConstraints::DEFAULT_FULL,
|
||||
});
|
||||
}
|
||||
profiles
|
||||
}
|
||||
|
||||
fn extension(id_byte: u8, payloads: &[u8]) -> TextExtension {
|
||||
TextExtension {
|
||||
extension_id: ExtensionId([id_byte; 16]),
|
||||
version: SemVer::new(1, 0, u32::from(id_byte)),
|
||||
required: false,
|
||||
chunks: payloads
|
||||
.iter()
|
||||
.map(|byte| TextChunk {
|
||||
kind: ChunkKind::ExtensionData,
|
||||
schema_version: SchemaVersion::V0,
|
||||
payload: vec![*byte],
|
||||
})
|
||||
.collect(),
|
||||
affected_object_kinds: vec![id_byte],
|
||||
edit_barriers: vec![id_byte, id_byte.wrapping_add(1)],
|
||||
}
|
||||
}
|
||||
|
||||
fn base(snapshot_byte: u8) -> TextCanonicalBase {
|
||||
TextCanonicalBase {
|
||||
snapshot_id: SnapshotId([snapshot_byte; 16]),
|
||||
covers_causal_frontier: FrontierBytes::from_bytes(vec![1, 2, snapshot_byte]),
|
||||
reduction_algorithm_version: ReductionAlgorithmVersion(1),
|
||||
profile_id: ProfileId::Full,
|
||||
root_schema_version: SchemaVersion::V0,
|
||||
root_payload: vec![0xA0, snapshot_byte],
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_documents() -> Vec<(&'static str, String)> {
|
||||
let minimal = TextDocument {
|
||||
document_id: DocumentId([1; 16]),
|
||||
lineage_id: None,
|
||||
profiles: profiles(false),
|
||||
extensions: Vec::new(),
|
||||
canonical_base: None,
|
||||
blobs: Vec::new(),
|
||||
envelopes: Vec::new(),
|
||||
};
|
||||
let lineage_custom = TextDocument {
|
||||
document_id: DocumentId([2; 16]),
|
||||
lineage_id: Some(LineageId([0x12; 16])),
|
||||
profiles: profiles(true),
|
||||
extensions: Vec::new(),
|
||||
canonical_base: None,
|
||||
blobs: Vec::new(),
|
||||
envelopes: vec![sample_envelope(1, 100)],
|
||||
};
|
||||
let extension_base_multi = TextDocument {
|
||||
document_id: DocumentId([3; 16]),
|
||||
lineage_id: None,
|
||||
profiles: profiles(false),
|
||||
extensions: vec![extension(1, &[1, 2])],
|
||||
canonical_base: Some(base(3)),
|
||||
blobs: Vec::new(),
|
||||
envelopes: vec![sample_envelope(2, 200), sample_envelope(3, 300)],
|
||||
};
|
||||
let rich = TextDocument {
|
||||
document_id: DocumentId([4; 16]),
|
||||
lineage_id: Some(LineageId([0x14; 16])),
|
||||
profiles: profiles(true),
|
||||
extensions: vec![extension(1, &[1, 2]), extension(2, &[3])],
|
||||
canonical_base: Some(base(4)),
|
||||
blobs: Vec::new(),
|
||||
envelopes: vec![
|
||||
sample_envelope(4, 400),
|
||||
sample_envelope(5, 500),
|
||||
sample_envelope(6, 600),
|
||||
],
|
||||
};
|
||||
|
||||
vec![
|
||||
("minimal", project_text_document(&minimal)),
|
||||
(
|
||||
"lineage_custom_profile",
|
||||
project_text_document(&lineage_custom),
|
||||
),
|
||||
(
|
||||
"extension_base_two_envelopes",
|
||||
project_text_document(&extension_base_multi),
|
||||
),
|
||||
("rich_document", project_text_document(&rich)),
|
||||
]
|
||||
}
|
||||
|
||||
fn replace_once(text: &str, anchor: &str, replacement: &str) -> String {
|
||||
assert!(
|
||||
text.contains(anchor),
|
||||
"vector-construction anchor is absent: {anchor}"
|
||||
);
|
||||
text.replacen(anchor, replacement, 1)
|
||||
}
|
||||
|
||||
fn lines_with_final_lf(lines: &[String]) -> String {
|
||||
let mut text = lines.join("\n");
|
||||
text.push('\n');
|
||||
text
|
||||
}
|
||||
|
||||
fn swap_first_lines(text: &str, first_head: &str, second_head: &str) -> String {
|
||||
let mut lines: Vec<String> = text.lines().map(str::to_owned).collect();
|
||||
let first = lines
|
||||
.iter()
|
||||
.position(|line| line.starts_with(first_head))
|
||||
.unwrap_or_else(|| panic!("vector-construction line is absent: {first_head}"));
|
||||
let second = lines
|
||||
.iter()
|
||||
.position(|line| line.starts_with(second_head))
|
||||
.unwrap_or_else(|| panic!("vector-construction line is absent: {second_head}"));
|
||||
lines.swap(first, second);
|
||||
lines_with_final_lf(&lines)
|
||||
}
|
||||
|
||||
fn swap_first_two_lines(text: &str, head: &str) -> String {
|
||||
let mut lines: Vec<String> = text.lines().map(str::to_owned).collect();
|
||||
let matches: Vec<usize> = lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, line)| line.starts_with(head).then_some(index))
|
||||
.collect();
|
||||
assert!(
|
||||
matches.len() >= 2,
|
||||
"vector construction needs two `{head}` lines"
|
||||
);
|
||||
lines.swap(matches[0], matches[1]);
|
||||
lines_with_final_lf(&lines)
|
||||
}
|
||||
|
||||
fn duplicate_first_line(text: &str, head: &str) -> String {
|
||||
let mut lines: Vec<String> = text.lines().map(str::to_owned).collect();
|
||||
let at = lines
|
||||
.iter()
|
||||
.position(|line| line.starts_with(head))
|
||||
.unwrap_or_else(|| panic!("vector-construction line is absent: {head}"));
|
||||
let duplicate = lines[at].clone();
|
||||
lines.insert(at + 1, duplicate);
|
||||
lines_with_final_lf(&lines)
|
||||
}
|
||||
|
||||
/// Every whole-document vector in stable committed order.
|
||||
pub fn document_vectors() -> Vec<TextVector> {
|
||||
let accepts = accept_documents();
|
||||
let minimal = &accepts[0].1;
|
||||
let lineage_custom = &accepts[1].1;
|
||||
let extension_base_multi = &accepts[2].1;
|
||||
let rich = &accepts[3].1;
|
||||
|
||||
let mut vectors: Vec<TextVector> = accepts
|
||||
.iter()
|
||||
.map(|(name, text)| (SURFACE, "accept", "-", *name, text.as_bytes().to_vec()))
|
||||
.collect();
|
||||
|
||||
let wrong_version = replace_once(
|
||||
minimal,
|
||||
"(text-projection (0 7 0))",
|
||||
"(text-projection (0 8 0))",
|
||||
);
|
||||
vectors.push((
|
||||
SURFACE,
|
||||
"reject",
|
||||
"wrong-header-version",
|
||||
"future_companion_version",
|
||||
wrong_version.into_bytes(),
|
||||
));
|
||||
|
||||
let blob = replace_once(
|
||||
minimal,
|
||||
"(profile full (0 1 0) (constraints 67108864 (retention 1 () true)))\n",
|
||||
"(profile full (0 1 0) (constraints 67108864 (retention 1 () true)))\n\
|
||||
(blob \"application/octet-stream\" () #x01)\n",
|
||||
);
|
||||
vectors.push((
|
||||
SURFACE,
|
||||
"reject",
|
||||
"blob-line",
|
||||
"unreferenced_blob",
|
||||
blob.into_bytes(),
|
||||
));
|
||||
|
||||
vectors.push((
|
||||
SURFACE,
|
||||
"reject",
|
||||
"out-of-order-sections",
|
||||
"canonical_base_before_extension",
|
||||
swap_first_lines(extension_base_multi, "(extension ", "(canonical-base ").into_bytes(),
|
||||
));
|
||||
|
||||
vectors.push((
|
||||
SURFACE,
|
||||
"reject",
|
||||
"repeated-singular-section",
|
||||
"lineage_repeated",
|
||||
duplicate_first_line(lineage_custom, "(lineage ").into_bytes(),
|
||||
));
|
||||
|
||||
vectors.push((
|
||||
SURFACE,
|
||||
"reject",
|
||||
"operation-envelope-order",
|
||||
"envelopes_reversed",
|
||||
swap_first_two_lines(extension_base_multi, "(envelope ").into_bytes(),
|
||||
));
|
||||
|
||||
vectors.push((
|
||||
SURFACE,
|
||||
"reject",
|
||||
"profile-declaration-order",
|
||||
"profiles_reversed",
|
||||
swap_first_two_lines(lineage_custom, "(profile ").into_bytes(),
|
||||
));
|
||||
|
||||
vectors.push((
|
||||
SURFACE,
|
||||
"reject",
|
||||
"extension-declaration-order",
|
||||
"extensions_reversed",
|
||||
swap_first_two_lines(rich, "(extension ").into_bytes(),
|
||||
));
|
||||
|
||||
let chunks_reversed = replace_once(
|
||||
extension_base_multi,
|
||||
"((chunk extension-data (schema 0 1) #x01) (chunk extension-data (schema 0 1) #x02))",
|
||||
"((chunk extension-data (schema 0 1) #x02) (chunk extension-data (schema 0 1) #x01))",
|
||||
);
|
||||
vectors.push((
|
||||
SURFACE,
|
||||
"reject",
|
||||
"extension-chunk-order",
|
||||
"extension_chunks_reversed",
|
||||
chunks_reversed.into_bytes(),
|
||||
));
|
||||
|
||||
let mut missing_lf = minimal.as_bytes().to_vec();
|
||||
assert_eq!(missing_lf.pop(), Some(b'\n'), "accept text ends in LF");
|
||||
vectors.push((
|
||||
SURFACE,
|
||||
"reject",
|
||||
"missing-trailing-lf",
|
||||
"final_lf_missing",
|
||||
missing_lf,
|
||||
));
|
||||
|
||||
vectors
|
||||
}
|
||||
|
||||
fn to_hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
}
|
||||
|
||||
fn from_hex(hex: &str) -> Option<Vec<u8>> {
|
||||
if hex.len() % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
(0..hex.len())
|
||||
.step_by(2)
|
||||
.map(|at| u8::from_str_radix(&hex[at..at + 2], 16).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Renders the drift-locked corpus file.
|
||||
pub fn render() -> String {
|
||||
let mut out = String::from(HEADER);
|
||||
out.push_str("\n# textproj.document\n");
|
||||
for (surface, verdict, class, name, text) in document_vectors() {
|
||||
out.push_str(&format!(
|
||||
"{surface} {verdict} {class} {name} {}\n",
|
||||
to_hex(&text)
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Parses a corpus file, skipping comments and blank lines.
|
||||
pub fn parse(text: &str) -> Result<Vec<Row>, String> {
|
||||
let mut rows = Vec::new();
|
||||
for (index, line) in text.lines().enumerate() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let fields: Vec<&str> = line.split_whitespace().collect();
|
||||
if fields.len() != 5 {
|
||||
return Err(format!(
|
||||
"line {}: expected 5 columns, got {}",
|
||||
index + 1,
|
||||
fields.len()
|
||||
));
|
||||
}
|
||||
rows.push(Row {
|
||||
surface: fields[0].to_owned(),
|
||||
verdict: fields[1].to_owned(),
|
||||
class: fields[2].to_owned(),
|
||||
name: fields[3].to_owned(),
|
||||
text: from_hex(fields[4]).ok_or_else(|| format!("line {}: bad hex", index + 1))?,
|
||||
});
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Checks one corpus surface without collapsing parser rejection with accepted
|
||||
/// normalization. `None` means this crate does not own `surface`.
|
||||
pub fn check(surface: &str, text: &[u8]) -> Option<CheckVerdict> {
|
||||
if surface != SURFACE {
|
||||
return None;
|
||||
}
|
||||
let text = match std::str::from_utf8(text) {
|
||||
Ok(text) => text,
|
||||
Err(error) => return Some(CheckVerdict::Rejected(format!("non-UTF-8 text: {error}"))),
|
||||
};
|
||||
let document = match parse_document(text) {
|
||||
Ok(document) => document,
|
||||
Err(error) => return Some(CheckVerdict::Rejected(error.to_string())),
|
||||
};
|
||||
let bundle = match serialize_document(&document, MemStore::new(), FileUuid([0x7E; 16])) {
|
||||
Ok(bundle) => bundle,
|
||||
Err(error) => {
|
||||
return Some(CheckVerdict::AcceptedNonCanonical(format!(
|
||||
"parse accepted, but serialization failed: {error}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let projected = match project_bundle(&bundle) {
|
||||
Ok(projected) => projected,
|
||||
Err(error) => {
|
||||
return Some(CheckVerdict::AcceptedNonCanonical(format!(
|
||||
"parse accepted, but re-projection failed: {error}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
if projected.as_bytes() == text.as_bytes() {
|
||||
Some(CheckVerdict::AcceptedCanonical)
|
||||
} else {
|
||||
Some(CheckVerdict::AcceptedNonCanonical(
|
||||
"parse accepted text that re-projected to different bytes".to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_rows(rows: &[Row]) -> Vec<String> {
|
||||
let mut failures = Vec::new();
|
||||
for row in rows {
|
||||
let Some(actual) = check(&row.surface, &row.text) else {
|
||||
failures.push(format!("{}: no checker owns this surface", row.surface));
|
||||
continue;
|
||||
};
|
||||
match (row.verdict.as_str(), actual) {
|
||||
("accept", CheckVerdict::AcceptedCanonical) | ("reject", CheckVerdict::Rejected(_)) => {
|
||||
}
|
||||
("accept", CheckVerdict::AcceptedNonCanonical(detail)) => failures.push(format!(
|
||||
"{}/{}: accepted, but not canonical: {detail}",
|
||||
row.surface, row.name
|
||||
)),
|
||||
("reject", CheckVerdict::AcceptedNonCanonical(detail)) => failures.push(format!(
|
||||
"{}/{} ({}): declared reject, but was ACCEPTED and normalized: {detail}",
|
||||
row.surface, row.name, row.class
|
||||
)),
|
||||
("reject", CheckVerdict::AcceptedCanonical) => failures.push(format!(
|
||||
"{}/{} ({}): declared reject, but was ACCEPTED as canonical",
|
||||
row.surface, row.name, row.class
|
||||
)),
|
||||
("accept", CheckVerdict::Rejected(error)) => failures.push(format!(
|
||||
"{}/{}: declared accept, but was rejected: {error}",
|
||||
row.surface, row.name
|
||||
)),
|
||||
(other, actual) => failures.push(format!(
|
||||
"{}/{}: unknown verdict {other}, got {actual:?}",
|
||||
row.surface, row.name
|
||||
)),
|
||||
}
|
||||
}
|
||||
failures
|
||||
}
|
||||
|
||||
/// Measures the corpus's actual reach. Reject counts include only vectors the
|
||||
/// parser really refused; an accidentally canonicalized reject therefore drops
|
||||
/// its class to zero instead of receiving credit from its label.
|
||||
pub fn reach(text: &str) -> Result<ReachCounts, Vec<String>> {
|
||||
let rows = parse(text).map_err(|error| vec![error])?;
|
||||
let mut reach = ReachCounts {
|
||||
extensions: 0,
|
||||
canonical_bases: 0,
|
||||
custom_profiles: 0,
|
||||
lineages: 0,
|
||||
multi_envelope: 0,
|
||||
reject_classes: BTreeMap::new(),
|
||||
};
|
||||
let mut failures = Vec::new();
|
||||
|
||||
for row in &rows {
|
||||
match row.verdict.as_str() {
|
||||
"accept" => {
|
||||
let text = match std::str::from_utf8(&row.text) {
|
||||
Ok(text) => text,
|
||||
Err(error) => {
|
||||
failures.push(format!("{}/{}: {error}", row.surface, row.name));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let document = match parse_document(text) {
|
||||
Ok(document) => document,
|
||||
Err(error) => {
|
||||
failures.push(format!("{}/{}: {error}", row.surface, row.name));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
reach.extensions += usize::from(!document.extensions.is_empty());
|
||||
reach.canonical_bases += usize::from(document.canonical_base.is_some());
|
||||
reach.custom_profiles += usize::from(
|
||||
document
|
||||
.profiles
|
||||
.iter()
|
||||
.any(|profile| matches!(profile.profile_id, ProfileId::Custom(_))),
|
||||
);
|
||||
reach.lineages += usize::from(document.lineage_id.is_some());
|
||||
reach.multi_envelope += usize::from(document.envelopes.len() > 1);
|
||||
}
|
||||
"reject" => {
|
||||
if matches!(
|
||||
check(&row.surface, &row.text),
|
||||
Some(CheckVerdict::Rejected(_))
|
||||
) {
|
||||
*reach.reject_classes.entry(row.class.clone()).or_default() += 1;
|
||||
}
|
||||
}
|
||||
other => failures.push(format!(
|
||||
"{}/{}: unknown verdict {other}",
|
||||
row.surface, row.name
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
if failures.is_empty() {
|
||||
Ok(reach)
|
||||
} else {
|
||||
Err(failures)
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies every verdict and the exact non-vacuity reach contract, returning
|
||||
/// the number of checked whole documents or all disagreements.
|
||||
pub fn verify(text: &str) -> Result<usize, Vec<String>> {
|
||||
let rows = parse(text).map_err(|error| vec![error])?;
|
||||
let mut failures = verify_rows(&rows);
|
||||
match reach(text) {
|
||||
Ok(actual) if actual == expected_reach() => {}
|
||||
Ok(actual) => failures.push(format!(
|
||||
"corpus reach mismatch: expected {:?}, got {actual:?}",
|
||||
expected_reach()
|
||||
)),
|
||||
Err(mut errors) => failures.append(&mut errors),
|
||||
}
|
||||
if failures.is_empty() {
|
||||
Ok(rows.len())
|
||||
} else {
|
||||
Err(failures)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_committed_corpus_matches_the_generator() {
|
||||
assert_eq!(
|
||||
COMMITTED,
|
||||
render(),
|
||||
"\n{PATH} is stale. Regenerate:\n \
|
||||
cargo run -q -p epiphany-testkit --example generate_vectors\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reference_implementation_agrees_with_every_vector() {
|
||||
match verify(COMMITTED) {
|
||||
Ok(count) => assert_eq!(count, 13, "the corpus has unexpectedly thinned"),
|
||||
Err(failures) => panic!(
|
||||
"{} disagreement(s):\n{}",
|
||||
failures.len(),
|
||||
failures.join("\n")
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// This test intentionally names TEXT identity. Comparing either bundle's
|
||||
/// bytes would be wrong: duplicate blobs collapse, layout is regenerated,
|
||||
/// and non-canonical accelerators vanish while semantics remain unchanged.
|
||||
#[test]
|
||||
fn every_accept_vector_satisfies_the_text_quantified_byte_equation() {
|
||||
for row in parse(COMMITTED)
|
||||
.expect("the committed corpus parses")
|
||||
.into_iter()
|
||||
.filter(|row| row.verdict == "accept")
|
||||
{
|
||||
assert_eq!(
|
||||
check(&row.surface, &row.text),
|
||||
Some(CheckVerdict::AcceptedCanonical),
|
||||
"{}/{} does not satisfy project(serialize(parse(T))) == T",
|
||||
row.surface,
|
||||
row.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_corpus_reach_is_exact_and_non_vacuous() {
|
||||
assert_eq!(
|
||||
reach(COMMITTED).expect("the committed corpus has measurable reach"),
|
||||
expected_reach(),
|
||||
"the corpus must reach 2 extensions, 2 canonical bases, 2 custom profiles, \
|
||||
2 lineages, 2 multi-envelope documents, and one real rejection in each class"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Epiphany Text Projection document vectors — format version 1
|
||||
#
|
||||
# Generated. Regenerate with:
|
||||
# cargo run -q -p epiphany-testkit --example generate_vectors
|
||||
# `epiphany_textproj::vectors::the_committed_corpus_matches_the_generator` fails
|
||||
# on drift, so a projection change must land here deliberately.
|
||||
#
|
||||
# One vector per line, space-separated:
|
||||
#
|
||||
# <surface> <verdict> <class> <name> <utf8-hex>
|
||||
#
|
||||
# NORMATIVE: Text Projection companion, `req:textproj:roundtrip` and
|
||||
# `req:textproj:strict-parse`. Every `accept` text must satisfy the exact-byte
|
||||
# equation project(serialize(parse(T))) == T. Every `reject` text must be
|
||||
# refused, not accepted and normalized to a different canonical spelling.
|
||||
#
|
||||
# The equation quantifies over TEXTS, not bundle bytes. Binary layout is freely
|
||||
# regenerated, duplicate blobs collapse, and non-canonical accelerators vanish;
|
||||
# all three preserve semantics and none weakens this text identity check.
|
||||
#
|
||||
# `class` names the rejection exercised. It is informative; the verdict and
|
||||
# document bytes are normative. `<utf8-hex>` is lowercase with no separators.
|
||||
|
||||
# textproj.document
|
||||
textproj.document accept - minimal 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a
|
||||
textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a
|
||||
textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a
|
||||
textproj.document accept - rich_document 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783034303430343034303430343034303430343034303430343034303430343034290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a
|
||||
textproj.document reject wrong-header-version future_companion_version 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a
|
||||
textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a
|
||||
textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a
|
||||
textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a
|
||||
textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a
|
||||
textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a
|
||||
textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783034303430343034303430343034303430343034303430343034303430343034290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a
|
||||
textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a
|
||||
textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929
|
||||
Loading…
Reference in New Issue