PipeWhy Normalization Contract C2 version 1.0 October 30, 2026 D2 user choice: "Strict representation by default; logical profile opt-in". Observations record the selected profile identity. Different profiles require explicit re-normalization from compatible retained source data before comparison. Representation means the typed scalar supplied by an adapter, not arbitrary file serialization syntax. Preserve original bytes/source encoding as bytes and retain schema metadata when those distinctions matter. Decoding a source must not silently claim original byte identity has been retained. PW-03.01 Profiles strict-v1 is the default. logical-v1 is opt-in and applies only the documented equivalences below. It never coerces bool/int/float/Decimal/text/bytes across types. Profile identity includes wire version, profile name and (for logical-v1) the Unicode normalization database version. Identity uses deterministic SHA-256 over a fixed profile descriptor, not Python hash or an unchecked truncated ID. PW-03.02 Absence MISSING is an exported singleton with its own type code. None/null is distinct. Empty strings/bytes, false and zero are present typed values, not absence. PW-03.03 Framing Wire version 1 uses flat uint64 words and little-endian serialization. Type codes: MISSING=0, NULL=1, BOOL=2, INT64=3, UINT64=4, FLOAT64=5, TEXT=6, BYTES=7, DECIMAL=8, TIMESTAMP=9. A scalar has a type code and payload words. Records are [wire_version=1, domain=2, field_count], followed by field_id, type_code, payload_word_count and payload words for each sorted field. Duplicate, negative and UINT64_MAX field IDs are rejected. Type and boundaries are never inferred from concatenated bytes. PW-03.04 Integers and booleans Bool is one word 0/1. Negative Python integers use INT64 with a two's-complement word and must be >= -2**63. Nonnegative integers use UINT64 and must be <= 2**64-1. Source widths are field schema metadata: preserve them there when meaningful. No float coercion, truncation or wrap on overflow. Exact supported built-in scalar types are dispatched; arbitrary scalar subclasses are not implicitly coerced. PW-03.05 Text and bytes Text is strict UTF-8 of Python Unicode scalar text; surrogates are rejected. strict-v1 performs no Unicode/case/whitespace folding. logical-v1 applies NFC, preserving case and whitespace. Bytes never decode or normalize. Payload is [byte_count, packed little-endian uint64 words...], with zero tail padding. Byte count preserves trailing zero bytes. Logical profile identity includes unicodedata.unidata_version to prevent silent changes across Python upgrades. PW-03.06 Timestamps Support exact datetime instances with no timezone, fixed datetime.timezone or ZoneInfo. Reject custom tzinfo and datetime subclasses. Strict timestamp words preserve year/month/day/hour/minute/second/microsecond, fold, timezone kind (0 naive, 1 fixed, 2 ZoneInfo), signed offset in microseconds as a two's-complement uint64 word, and a packed UTF-8 zone key/fixed-zone name. Naive values stay distinct from UTC-aware values. logical-v1 converts aware values to UTC and sets fold=0; naive values remain naive. Conversion overflow or invalid timezone evidence is an error. Python datetime has microsecond resolution; nanosecond input needs an explicit later typed extension, never silent rounding. No source encoding/formatting identity is implied. PW-03.07 Decimals Use Decimal.as_tuple without ambient-context arithmetic. Strict output preserves sign, exponent, coefficient digit count and integer coefficient. Decimal words: [sign, exponent_kind, exponent_sign, exponent_magnitude, digit_count, coefficient_word_count, little-endian base-2**64 coefficient words...]. Exponent kinds: 0 finite, 1 infinity, 2 quiet NaN, 3 signaling NaN. Special exponents have sign/magnitude zero; finite exponent magnitude must fit uint64. Zero coefficient has one zero word. No float conversion or context rounding. logical-v1 strips trailing finite coefficient zeros and adjusts exponent exactly; finite zeros become positive exponent-zero zero. Decimal NaNs become quiet with no sign or payload; signed infinities remain distinct. An empty special NaN coefficient has digit_count=0 and one zero coefficient word. PW-03.08 Floating point FLOAT64 is the exact binary64 word for Python float. Strict output preserves negative zero, NaN sign/payload and signed infinities without arithmetic. logical-v1 maps zeros to positive zero and NaNs to 0x7ff8000000000000. Other values remain bit-exact. Float and Decimal remain distinct types. PW-03.09 Records and containers Fields sort by stable field ID, not mapping insertion order; field association is retained before aggregation. Repeated records remain repeated contributions. Container/dataset order semantics are explicit fingerprint-layer contracts. No nested object is accepted as a scalar. Batch boundaries cannot alter output. PW-03.10 Schema changes Added/removed/retyped fields alter framing and schema identity. Renames retain identity only under recorded source schema policy. No implicit field exclusions, widening or type coercions. Adapters select exclusions explicitly and record them. Unsupported types fail visibly; adapters preserve read/coverage evidence. Scope: normative specification for PW-03.11-.23 implementation, not a claim that all source adapters, original-byte comparisons or higher timestamp precision exist. Contract review: PW-03.01-.10 are defined by the ten numbered sections above. The contract distinguishes decoded typed values from source bytes, keeps complete field association, bounds integer input, avoids ambient Decimal rounding, records Unicode version and rejects unsupported schema conversions explicitly. Implementation and consumer handoff (PW-03.23) Public entry points are normalize.scalars.encode_value and normalize.records.encode_record; both default to Profile.STRICT. Scalar results carry TypeCode and owned array('Q') words. Records carry owned words and the full profile identity string. Arrays are mutable caller-owned results; copy/freeze at persistence boundaries. No cached or shared output storage is exposed. tests/fixtures/normalization_v1.json is the portable C2 acceptance fixture. Every supported type has hand-derived expected words. Expected uint64 words are decimal strings; inputs carry explicit types, including float bit patterns. Consumers must parse those strings exactly, not through binary64 JSON numbers. On-wire words serialize little-endian; native array.tobytes is appropriate only on little-endian hosts (the C1 CPU views reject other hosts). tests/unit/test_normalization_model.py verifies C2 payloads, null/missing tags, offsets and validity cross the C1 values-table validators and reconstruct the same domain-separated record. It preserves field IDs and full policy identity. A local observation policy_id must resolve through persisted metadata to this full identity; it is not a truncated SHA-256 and cannot substitute for compatibility checking. Fingerprint handoff consumes the fixture, stable field boundaries and profile identities in implemented C3/native tests. D3 selects accidental drift/corruption detection and D4 selects the experimental C3 reference profile; its independent production collision review remains unresolved. Normalization itself makes no collision, security or kernel performance guarantees. Shared flat-batch protocol (PW-09.13 implementation component) normalize.batches.RecordBatch carries profile_id, words, offsets and lengths. The three buffers use C1 native contiguous uint64 storage. batch_views validates equal offset/length counts, nonoverflowing bounded slices and every complete C2 field frame before returning borrowed views. Physical pool order may differ from logical record order; unused pool words are permitted. An empty batch contains zero records. validate_record_words enforces version/domain header, ordered unique non-sentinel field IDs, recognized tags and exact framing. Canonical scalar payload semantics remain the C2 producer's responsibility. No buffer copies or default profile inference occur here; consumers compare the declared profile with their explicit context. The returned views retain exporters, and all source aliases remain stable during use. The native CPU engine and reference record hashing reuse this implementation. fingerprint.cpu.RecordBatch remains an alias of this same class for existing callers.