PipeWhy Local Fixture Adapter Contract C4 version 1.0
October 30, 2026

PW-06.01 Stable fixture identity
The reference source namespace is pipewhy.local.fixture.v1. It is a deterministic
test source, not a filesystem watcher or an enterprise connector. Identity comes
from explicit IDs in the source manifest, never Python hash, paths, enumeration
order, process addresses or timestamps. Moving the fixture directory preserves
identity. The source namespace is retained with every local ID.

Artifacts are 0=fixture root, 1=A, 2=B, 3=C, 4=D. The root has parent NULL_ID;
datasets A/B/C/D have parent 0. Names use append-stable label IDs in this exact
order: fixture, A, B, C, D. Artifact kind 0 means container, 1 means dataset.
Containment and data lineage are distinct relations.

Version IDs are baseline-v1, mutation-v1 and missing-v1, with local source-version
IDs 0, 1 and 2 respectively. Version strings and integer IDs are explicit mappings,
not hashes or fingerprints. A requested historical version is pinned for the whole
operation; there is no implicit latest fallback. Unknown versions fail visibly.
Baselines and deltas are immutable fixture data; reads never mutate stored input.

Each dataset has two records identified by field 0=row_id, values 0 and 1.
Field 1=amount is integer cents for A/B/D and Decimal dollars for C. A field's
source schema records that distinction; normalization does not coerce it away.
The row identifier is part of the record payload and preserves row association.

Baseline A/B/D amounts are 100 and 250 cents; C amounts are Decimal 1.00 and 2.50.
The mutation changes exactly B's row_id=1 amount from 250 to 251 cents. C and D
remain unchanged, creating deliberate divergence along the declared lineage.
The missing version retains baseline identity/schema but makes C unreadable with
an explicit injected-read-failure reason. Unreadability is not an empty dataset.

Lineage edge IDs are 0=A->B COPY, 1=B->C TRANSFORM, 2=B->D COPY. Evidence IDs
0/1/2 identify explicit source declarations. Transform ID 0=cents_to_dollars_v1
declares exact amount_dollars * 100 == amount_cents per matching row_id, with
unchanged row count and keys. COPY edges have NULL_ID transform. These declarations
are evidence of intended provenance; they do not prove current content equality.

PW-06.02 Protocol and fixture examples
Fixture input files are UTF-8 JSON supplied as a directory to LocalFixtureAdapter.
The fixture namespace and version/schema IDs are validated; unknown schemas,
duplicate IDs, malformed inputs and unsupported typed values fail visibly. No
external services or optional dependencies are required. Runtime code does not
depend on the repository's test directory or bundle test fixtures into the wheel.
The adapter captures its source data at construction and returns independent owned
arrays. Altering returned buffers cannot mutate the captured source or later reads.

capabilities() reports versioned schema, immutable-version consistency, pinned
historical reads, explicit lineage, supported scalar types and injected-failure
support. This describes only this deterministic source, not enterprise guarantees.

discover(scope=0, batch_size=1024) returns stable increasing artifact IDs as C1
artifact-table batches, exported label buffers, scope and COMPLETE discovery
coverage. Root scope includes all five artifacts; dataset scope includes only that
dataset. Unknown scope and invalid batch size raise ValueError. No partial results
are reported as complete. For root scope, batch_size=2 produces IDs [0,1],[2,3],[4].

read(artifact_id, source_version, profile=STRICT, batch_size=1024) returns source
identity, pinned version, full profile identity, status/reason, expected/returned
record counts and owned value batches. Each batch has ordered record IDs plus a
C1 values table; all fields for a record remain in the same batch. Values retain
field IDs and typed payload offsets/lengths. Missing and null use separate C2 tags
and zero validity bits. Value IDs are explicit fixture IDs, unique across artifacts
within the source version; they are never row positions reassigned by batching.
The profile identity is carried beside every read result, not inferred from words.

Read statuses are COMPLETE and UNAVAILABLE. UNAVAILABLE returns no value batches
with a specific reason: UNKNOWN_VERSION, ARTIFACT_NOT_FOUND, NOT_DATASET or
INJECTED_READ_FAILURE. Unknown/inapplicable expected counts are None, never a
fabricated successful zero. Injected failure for C reports expected=2, returned=0.
An available empty dataset would be COMPLETE with expected=returned=0. Malformed
input, unsupported normalization or invalid arguments raise their explicit errors;
they are not recast as source unavailability. The scan coordinator must preserve
both statuses and exceptions in scan coverage evidence.

lineage(scope=0) returns C1 edge columns and explicit evidence/transform identities.
Scope is the induced artifact subgraph: both endpoints must be in scope. Coverage
is COMPLETE_WITHIN_SCOPE, not a claim about external relationships. Root scope
returns all three edges; dataset-only scope has no internal edges. Metadata marks
each relationship as a declared intent; content verification belongs to later tasks.

Fixture files use typed scalar objects: {"type":"int","value":"250"} and
{"type":"decimal","value":"2.50"}; integer strings avoid JSON rounding.
Baseline carries all artifact/field/record identities. mutation.json names exactly
one replacement by artifact/record/field ID. transform.json declares the invariant;
lineage.json declares edges and evidence; missing.json declares unreadable IDs.
Expected results are authored independently, not copied from normalizer output.

Implementation notes (PW-06.08-.13)
Import LocalFixtureAdapter from pipewhy.adapters.local and supply the directory.
Construction captures all five JSON inputs and validates the baseline, mutation,
failure evidence, lineage references and transform declaration before serving data.
Computation uses flat C1 arrays; transient JSON objects are not the retained value
or graph model. Captured values are normalized separately for both supported C2
profiles. All C4 result IDs carry the source namespace in their enclosing result.

Discovery.batches is a tuple of owned artifact column mappings. Discovery.labels
is an independent read-only UTF-8/offset/length snapshot loadable by LabelDictionary.
ReadResult.batches contains ValueBatch(record_ids, columns); C1 value rows repeat
the record's two fields, while record_ids contains one entry per complete record.
Value IDs and per-record ordering survive batch-size changes; payload offsets and
validity storage are repacked for each output batch. No borrowed source buffers
are exposed. The fixture is small and captured in memory; this is not a bounded
streaming implementation for arbitrary enterprise sources.

LineageResult.evidence and .transforms are immutable UTF-8 JSON bytes preserving
the explicit declarations, loadable with json.loads. Edges remain flat integer
columns. A copy/transform declaration establishes intent, not verified equality.
Available empty datasets return COMPLETE with expected=returned=0 and no batches.
File, JSON, schema, type and normalization errors propagate, retaining failure
context for the coordinator rather than being swallowed as successful coverage.
