tidewaterdata
by J-X0
README.md
# tidewaterdata (Project Tidewater)
Record-level lineage with replayable transformations, delivered as an MCP
server, for data-quality work at Tallowfield Technologies.
## The problem
Freight and logistics records arrive dirty: whitespace from CSV exports,
inconsistent casing on port and carrier codes, dates in half a dozen formats,
missing fields. Cleaning them is easy. Proving *what* was changed, *why*, and
that the cleaned record genuinely follows from the raw one is the hard part —
and it is what an auditor or a downstream dispute actually needs.
tidewaterdata makes every change go through a named, parameterised,
deterministic transformation and records each application with content hashes on
both sides. Given the original record and the recorded lineage you can **replay**
the whole history and prove the stored result follows from the stored steps — or
find the exact step where something was altered.
## How the CPU-only constraint shapes the design
The target environment is **CPU-only, with no GPU**, and in practice keeps a
small, often-isolated runtime footprint. That drives concrete choices:
- Correctness rests on canonical JSON + stdlib `blake2b` hashing and pure-Python
transforms. Nothing wants an accelerator. (ADR 0002)
- The runtime core has **no third-party dependencies**, including the MCP server
itself, which implements the JSON-RPC stdio protocol directly. (ADR 0001)
- Any model/LLM behaviour goes through a provider interface. The default
`StubProvider` is deterministic and offline, so the whole test suite runs with
no API key and no network. If the networked `RealProvider` is selected but
unconfigured — the normal case on an isolated box — the service degrades to the
stub with a logged warning instead of failing. (ADR 0004)
`tests/test_offline_cpu.py` holds this line: it runs the full flow with the
socket layer stubbed out and asserts no GPU/model libraries were imported.
## Architecture
```
MCP client (stdio JSON-RPC) shell
| |
tidewaterdata/server.py tidewaterdata/cli.py
\_____________ _____________/
\/
tidewaterdata/service.py
validate -> run core -> time -> structured log (stderr)
|
validation.py config.py lineage.py (Pipeline / replay / verify_chain)
|
transforms.py (registry) hashing.py (canonical JSON + blake2b)
|
providers/ base | stub (offline) | real (networked)
```
The core (`lineage`, `transforms`, `hashing`, `types`) knows nothing about
transport. `service.py` is the one place that validates input, times work, and
logs. `server.py` and `cli.py` are thin adapters over it. Transformations are
identified by `(name, params)` and resolved through a registry, so a lineage
serialises to pure JSON and replays anywhere without shipping code. (ADR 0003)
Decision records for the contested calls are in [`docs/adr/`](docs/adr/).
## Install
```
make install # creates .venv and installs the package with dev extras
```
`make` uses `PY ?= .venv/bin/python`; override with `make test PY=/path/to/python`.
The core imports only the standard library, so you can also run it straight from
a checkout without installing anything:
```
python3.12 -m tidewaterdata list-transforms
```
## Quickstart
Every command below runs against a checkout with `python3.12 -m tidewaterdata`;
after `make install`, the console script `tidewater` is equivalent.
List the available transforms:
```
python3.12 -m tidewaterdata list-transforms
```
Clean a record and get back the result plus a replayable lineage:
```
echo '{"record": {"id": "1", "name": " acme freight "},
"pipeline": [{"name": "trim_whitespace", "params": {"field": "name"}},
{"name": "collapse_spaces", "params": {"field": "name"}}]}' \
| python3.12 -m tidewaterdata clean -i -
```
The `result.name` is `"acme freight"`, and `lineage` carries the per-step hashes
and diffs. Save the whole `{record, lineage}` and verify it later:
```
echo '{"record": {"id": "1", "name": " acme freight "},
"lineage": { ...the lineage printed above... }}' \
| python3.12 -m tidewaterdata replay -i -
```
`replay` prints `{"result": ..., "verified": true}` on success, or exits `2`
with a `replay mismatch at step N` message if the record or lineage was altered.
Run the MCP server (reads newline-delimited JSON-RPC on stdin):
```
python3.12 -m tidewaterdata serve
```
Library use:
```python
from tidewaterdata import Pipeline, Transformation, replay
rec = {"id": "shp-1", "name": " acme freight ", "port": "lax"}
pipe = Pipeline([
Transformation("trim_whitespace", {"field": "name"}),
Transformation("collapse_spaces", {"field": "name"}),
Transformation("to_upper", {"field": "port"}),
])
lineage, cleaned = pipe.run(rec)
assert replay(rec, lineage) == cleaned # raises ReplayMismatch if tampered
```
## MCP tools
The server exposes four tools via `tools/list` / `tools/call`:
| Tool | Arguments | Returns |
|---|---|---|
| `list_transforms` | none | available transform names |
| `clean_record` | `record`, `pipeline` | `{result, lineage}` |
| `replay_lineage` | `record`, `lineage` | `{result, verified}` |
| `suggest_transforms` | `record`, `required_fields?` | `{suggestions}` |
Validation failures and replay mismatches come back as a normal `tools/call`
result with `isError: true` and a message; malformed JSON or an unknown method
comes back as a JSON-RPC error.
## Transforms
`trim_whitespace`, `collapse_spaces`, `to_upper`, `to_lower`, `default_missing`,
`drop_field`, `rename_field`, `coerce_int`, `standardize_date`. Register your own
with `@tidewaterdata.register("name")` — a transform is a pure `dict -> dict`
function that must not mutate its input.
## Configuration
All optional, read from the environment at startup and validated there (a bad
value fails immediately with a `configuration error`):
| Variable | Default | Meaning |
|---|---|---|
| `TIDEWATER_PROVIDER` | `stub` | `stub` (offline) or `real` (networked) |
| `TIDEWATER_MAX_RECORD_BYTES` | `1000000` | reject records larger than this |
| `TIDEWATER_MAX_STEPS` | `100` | reject pipelines longer than this |
| `TIDEWATER_MAX_BATCH` | `1000` | batch-size cap (reserved; see limitations) |
| `TIDEWATER_LOG_LEVEL` | `INFO` | `DEBUG`..`CRITICAL` |
`Config.from_file(path)` overlays these defaults with keys from a JSON object.
Logs are one JSON object per line on **stderr** (stdout is the protocol/result
stream). The size and length limits are the defence against resource
exhaustion: oversized input is rejected before any work is done.
The `real` provider additionally reads `TIDEWATER_PROVIDER_ENDPOINT` and
`TIDEWATER_PROVIDER_API_KEY`, and needs the `real` extra
(`pip install -e .[real]`, which pulls in `httpx`).
## Test
```
make test # or: python3.12 -m pytest
```
CLI exit codes: `0` success, `2` bad input / validation / missing file / replay
mismatch, `1` unexpected.
## Known limitations
- **No batch tool yet.** `TIDEWATER_MAX_BATCH` is wired into config but there is
no `clean_batch` MCP tool; records are cleaned one at a time. The cap exists so
the limit is enforced the moment batching lands.
- **Replay depends on transform behaviour staying stable across versions.**
Changing what a registered transform does will break replay of older lineages —
surfaced as a `ReplayMismatch`, not a silent wrong answer, but it is a real
compatibility obligation. (ADR 0003)
- **16-byte hashes detect tampering and drift, not a determined forger** who
recomputes the whole chain. Signing the final lineage is a future option.
(ADR 0002)
- **The `real` provider's response contract is provisional.** It assumes a
`{"suggestions": [...]}` shape; see the TODO in `providers/real.py` pending a
frozen endpoint spec.
- **No deployment wrapper.** systemd/container packaging is out of scope for this
delivery.
---
*Tallowfield Technologies is an illustrative client; this repository is a self-directed reference implementation built to work end to end.*
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues