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.
