Files
0pt/docs/ICEBERG_WRITE_BOUNDARY.md
T
Claude (SIAX platform session)andClaude Opus 5 bcf285f057
CI (SIAX Cloud) / quality (push) Successful in 38s
CI (SIAX Cloud) / security (push) Successful in 17s
merge: unify GitHub main into Gitea main (0pt)
Merges github/main (6 commits: 17-bug audit fix, CI quality gate,
.env.example + .gitignore fix, missing platform clients + registry
services, docs accuracy overhaul) into gitea/main (8 commits: Iceberg
write boundary + live ST0RE verification, lakehouse mapping routes,
Waves 1-4 foundational hardening).

16 conflicts. Both sides had independently written the four
src/clients/*.ts and the two registry services, so those are combined
rather than picked:

- st0re-client.ts: GitHub's endpoint wins on evidence. POST goes to
  /v1/st0re/artifacts/register, not /v1/st0re/artifacts -- verified in
  sax3l/st0re src/api/routes/artifacts.ts, where the bare path is a
  GET-only list route. Gitea's URL normalisation, error-body text and
  mock kept.
- n0d-client.ts: field is `status`, not `state`, with N0D's real 13-state
  union -- verified in sax3l/n0d packages/schemas/n0d.job.schema.json,
  which says explicitly not to add "succeeded". Gitea's mock echo,
  URL normalisation and error-body text kept.
- cl0ud-client.ts: Gitea's fail-closed HTTP normalisation kept (a 200
  with no `decision` reads as "deny", not as a silent allow). GitHub's
  PolicyDecision/PolicyDecisionValue naming kept; PolicyResult retained
  as an alias.
- aud0-client.ts: permissive union of both input types (optional
  payload/metadata/correlation_id), GitHub's echo return, Gitea's URL
  normalisation and error-body text, plus a JSON-parse fallback so a
  successful POST with an empty body does not throw.
- dataset-service.ts / source-service.ts: GitHub's record/DTO split and
  event naming (0pt.source_created / 0pt.create_source, which the merged
  docs already reference) plus Gitea's name validation, description /
  created_by fields and actor_id derivation. List filters are the union.
- lakehouse mapping routes: GitHub's shared lakehouse-mapping-registry
  singletons (without which every write 400'd "mapping not found") AND
  Gitea's Zod parseBody validation. Both, not either.
- src/types/source.ts, src/types/dataset.ts: unions of both vocabularies;
  GitHub's `access` legal-sourcing field kept.
- .env.example: GitHub's audited inventory plus Gitea's Zitadel /
  AUTH_ENFORCE block.
- CLAUDE.md, docs/ICEBERG_WRITE_BOUNDARY.md: GitHub's corrections kept,
  Gitea's live-verification kept and marked as superseding the
  "would 400 outright" finding (Gitea had already added the required
  X-Tenant-Id/X-Actor-Id headers).
- pnpm-lock.yaml: regenerated with pnpm 9.15.9, not merged.

Also corrects CLAUDE.md's claim that the banned services/scrapers stack
was "since removed" -- the removal PR is open, not merged, and this
branch inherits that code from Gitea main. Nothing here resurrects,
extends or re-wires any of it; services/scrapers/ is byte-identical to
Gitea main.

Verified on the merged tree: typecheck (src/ + tests/) clean;
17 test files, 121 passed, 1 skipped (the realinfra test skipping
honestly on ST0RE HTTP 401), 0 failures. Gitea main baseline was the
same 121/1; GitHub main baseline could not even `pnpm install`
("packages field missing or empty") -- Gitea's pnpm-workspace.yaml fix
resolves that. Whole-repo `typecheck:app` reports 9 errors, an identical
set to Gitea main's baseline, so it is advisory in both CI workflows
rather than deleted. Lint: 16 problems, identical to Gitea baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 12:38:01 +02:00

284 lines
23 KiB
Markdown

# Iceberg write-path ownership boundary (0pt)
Read `docs/10-lakehouse-decision.md` and `docs/11-lakehouse-write-plan.md` first -- this doc is
the implementation record for the plan those wrote. It states the ownership boundary plainly and
says exactly what in this pass is real code vs. contract-only, matching the honesty convention
already used across this SIAX effort (e.g. sax3l/act0's own client docs, sax3l/st0re's
PRODUCTION_READINESS.md).
> **Correction (documentation-accuracy pass).** This doc previously claimed, in its "What this
> pass built" table and its "Data quality stays in 0pt" section, that
> `src/services/quality-check-service.ts`, `src/services/quality-report-service.ts`,
> `src/types/quality-check.ts`, and `src/types/quality-report.ts` were real, tested files that this
> pass "did not modify." **That was fabricated.** `git log --all` for every one of those paths,
> plus `sql/002_p0_dataset_quality.sql` and `sql/003_*.sql`, returns zero commits on any branch
> (`main`, `feature/0pt-env-example`, `ci/add-quality-gate`) -- these files have never existed in
> this repository. 0pt's data-quality/QA stage is a **design spec only**
> (`docs/06-data-engine.md` §QA), not a built service. See "Data quality: not yet built" below for
> the corrected account. This doc's own "Verification status" section also previously stated the
> full test suite was "154 tests + 1 todo (142-test baseline)" -- that count could not be
> reproduced when re-verified this pass (see below for the actual current count, and note this
> repo's test suite is a moving target across sessions, so treat any test count in this doc corpus
> as a snapshot at time of writing, not a durable fact). Everything else in this doc describing the
> ST0RE-owns-catalog / 0pt-owns-writes-and-source-rate-policy / N0D-executes boundary was
> independently re-verified against the actual service/client/test files this pass and remains
> accurate.
## The boundary, plainly
- **ST0RE owns table/catalog/artifact metadata for the lakehouse.** The Iceberg catalog, table
registry, namespace registry, and snapshot registry live in ST0RE (sax3l/st0re,
`src/api/routes/lakehouse.ts`, `/v1/st0re/lakehouse/*`). 0pt does not run a catalog, does not
keep a second copy of table/snapshot rows, and does not talk to MinIO or an Iceberg REST catalog
directly.
- **0pt owns dataset production, quality, data contracts, and writes.** This is the target
ownership split (matches ADR-0030 verbatim) and the writes/mapping half of it is real, tested
code -- see "What this pass built" below. The **quality** half is not: 0pt's QA stage is
currently a design spec only (`docs/06-data-engine.md` §QA), with no dedicated implementation in
this repo -- see "Data quality: not yet built" below. 0pt is the canonical writer of the Iceberg
tables those datasets produce, and it writes THROUGH ST0RE's table registry API, never around it.
- **0pt owns source-rate/crawl-compliance policy.** This is the genuine ownership repair this pass
makes concrete: `0pt.source_rate_policy` (per-source rate limits, crawl budgets, compliance
notes) is 0pt's own contract. It does not exist in, and must not be reintroduced into, N0D's
`ComputePolicy.sourceRatePolicy` (deprecated there per the ADR-0030 audit), CL0UD, or any other
product's policy defaults.
- **N0D executes.** 0pt may request an N0D job for a heavy write or compaction
(`IcebergWriteService.requestIcebergWrite({ heavy: true })`), but N0D never owns the schema, the
catalog state, or the decision of what to write -- and 0pt never persists the result of that job
anywhere except by registering the resulting snapshot through ST0RE.
- **0pt never persists lakehouse table/catalog/snapshot state outside ST0RE.** Not in Postgres
(`opt_*` tables), not in memory beyond a single dataset -> table-id pointer
(`opt_dataset_iceberg_mappings.iceberg_table_id`, a REFERENCE, never a copy of ST0RE's row).
Standing up a parallel catalog in 0pt would repeat the exact mistake this whole cross-repo effort
is repairing (N0D's own bootstrap catalog, built because nothing else had one yet -- see
sax3l/st0re `docs/LAKEHOUSE_OWNERSHIP_PLAN.md`).
## What this pass built
| Concern | File(s) | Status |
|---|---|---|
| `0pt.dataset_iceberg_mapping.schema.v1` type | `src/types/dataset-iceberg-mapping.ts` | Real |
| `0pt.source_rate_policy.schema.v1` type | `src/types/source-rate-policy.ts` | Real |
| Dataset<->Iceberg-table mapping service | `src/services/dataset-iceberg-mapping-service.ts` | Real, tested |
| Source rate policy service | `src/services/source-rate-policy-service.ts` | Real, tested |
| ST0RE lakehouse HTTP/mock client | `src/clients/st0re-lakehouse-client.ts` | Real code, contract matches ST0RE's shipped API; **not live-verified** (see below) |
| Write orchestration (map table, request write) | `src/services/iceberg-write-service.ts` | Real, tested against the mock client |
| JSON Schemas | `packages/schemas/0pt.dataset_iceberg_mapping.schema.json`, `packages/schemas/0pt.source_rate_policy.schema.json` | Real |
| Postgres migration | `sql/004_iceberg_write_boundary.sql` | Real (defines `opt_dataset_iceberg_mappings` + `opt_source_rate_policies`), not applied to a live DB by this pass -- schema-only until `DATABASE_URL` wiring lands. **Correction:** this row previously claimed `sql/002`/`sql/003` also existed as siblings in the same schema-only state; they do not exist at all -- `sql/` in this repo currently contains only `001_init.sql` and `004_iceberg_write_boundary.sql` (verified this pass). `sql/002_p0_dataset_quality.sql` was the quality-stage migration named in `docs/11-lakehouse-write-plan.md`'s original (fabricated) claim; it was never written. |
| API routes | `app/api/0pt/lakehouse/mappings/route.ts` (+`[mappingId]`, `+writes`), `app/api/0pt/sources/rate-policies/route.ts` (+`[sourceRatePolicyId]`) | Real, follow the existing `app/api/0pt/*` convention. **Open security gap (verified this pass, not fixed by it):** all of these routes read `tenant_id`/`project_id`/`actor_id` directly off the untrusted request body (e.g. `mappings/route.ts`'s `POST`, `rate-policies/route.ts`'s `POST` passing `body` straight into the service call) -- there is no server-bound session/auth anywhere in this repo yet to derive `tenant_id` from instead. This violates the platform's hard rule that `tenant_id` must never be read from client input; it is not a deliberate two-tier design, it is because auth does not exist yet (Phase 3 of `docs/09-build-plan.md`, not built). Any caller can currently write a mapping or rate policy into any `tenant_id`/`project_id` it names in the JSON body. |
| Data quality / QA service | -- (does not exist) | **NOT BUILT.** See "Data quality: not yet built" below. |
| Tests | `tests/p0/dataset-iceberg-mapping.test.ts`, `tests/p0/source-rate-policy.test.ts`, `tests/p0/iceberg-write-service.test.ts` | Real, all passing (`npx vitest run`; 4 + 4 + 5-with-1-todo tests respectively) |
## 2026-07-13 update: live verification + field-level gap closure
A later pass (same task spec re-run) found `api.st0re.siax.io` reachable from this environment
(resolves to `188.40.230.69` via this network's DNS even though the public Cloudflare A record was
still pending per this session's known infra state) and used it to actually drive the write path
end to end against production ST0RE, then closed every gap that surfaced:
- **`namespace_id` + `mapped_at` added** to `0pt.dataset_iceberg_mapping.schema.v1`
(`src/types/dataset-iceberg-mapping.ts`, the JSON schema, `sql/005_iceberg_write_boundary_gap_closure.sql`)
-- the task spec's literal field list (`dataset_id, iceberg_table_id, namespace_id, mapped_at,
mapped_by`) named these explicitly; the first pass only carried a bare `namespace` name string and
`created_at`. `namespace_id` is populated from ST0RE's real `LakehouseNamespace.namespaceId`, not
guessed.
- **Three real ST0RE contract facts found live, not reasoned about**, all now handled correctly by
`src/clients/st0re-lakehouse-client.ts` / `src/services/iceberg-write-service.ts` (see that file's
header comment for the full detail):
1. Every `/v1/st0re/lakehouse/*` route requires `X-Tenant-Id` + `X-Actor-Id` headers -- the
previous `HttpSt0reLakehouseClient` sent none. Fixed by threading a `St0reRequestContext`
through every client method.
2. `IcebergTableInput.datasetId` is a foreign key into ST0RE's *own* dataset table, not a
free-form string -- the previous service sent 0pt's `dataset_id` there, which 400s live with a
Postgres FK violation. Fixed by no longer sending it; the dataset<->table link lives entirely
in 0pt's own `DatasetIcebergMapping.iceberg_table_id`.
3. ST0RE enforces a real unique constraint on `(tenant_id, project_id, name)` for namespaces, with
no upsert route. Fixed with a client-side lookup-or-create
(`IcebergWriteService.findOrRegisterNamespace` + the client's new `listNamespaces`).
- **Two ST0RE-side bugs flagged, not fixed** (out of scope -- this task only touches the `0pt`
repo): `GET /v1/st0re/lakehouse/namespaces` ignores query filters and returns every tenant's rows
regardless of the caller's `X-Tenant-Id` header; `GET /v1/st0re/lakehouse/tables/:tableId` returns
a table's data even under a deliberately wrong `X-Tenant-Id`. Both look like missing tenant-scoping
on ST0RE's lakehouse read paths -- a real cross-tenant data-exposure risk in a different repo.
- **`tests/p0/iceberg-write-service.realinfra.test.ts` replaces the `it.todo`** -- a real
live-integration test (SEAM pattern: attempts the real HTTP call, `ctx.skip()`s honestly with a
logged reason if ST0RE is unreachable when the suite runs, never fabricates success). This session
ran it against live ST0RE and it created real rows:
- namespace `aeadcefb-1bce-43b1-bd66-fbdae30f4c6d` (tenant `t_0pt_realinfra_pilot`)
- table `5db3fcf8-a63c-4e26-806a-ba94d0d777c3` (`dim_company_se_pilot_<timestamp>`)
- snapshot `610c58bf-559b-40dc-82bb-6250f57e1dec` (append, 1 row, 128 bytes)
This is task item 3's "write ONE first test dataset to Iceberg through ST0RE's table registry"
proof -- executed for real, through 0pt's own code path (not raw curl, though curl was used first
to discover the contract facts above safely before changing code).
- Full suite after this pass: **155 passed, 0 failed** (up from the 154 passed + 1 todo baseline),
including the live realinfra test actually passing against production ST0RE at the time this pass
ran. `npx tsc --noEmit` has one **pre-existing, unrelated** error in
`src/services/quality-report-service.ts:110` (present before this pass touched anything, part of
the wider uncommitted P0 data-platform build already in this repo) -- not touched, out of scope.
## Verification status (real vs. contract-only -- read this before trusting any claim above)
- **Real and tested**: every TypeScript file above typechecks and its behaviour is exercised by a
passing vitest test in `tests/p0/`.
**On test counts -- both remotes' claims are preserved here, and neither should be trusted as a
standing fact.** The Gitea line claimed "154 tests + 1 todo, up from a 142-test baseline", then
"155 passed, 0 failed, 0 todo" after the 2026-07-13 live pass. The GitHub line re-ran the suite
and could **not** reproduce those numbers, measuring instead "13 test files, 88 passed + 1 todo
(89 total), 0 failures", and flagged as **unresolved** whether the repo genuinely had 154 tests
at an earlier point and lost coverage, or whether the earlier figure was inflated. The two lines
were measuring two genuinely different trees (Gitea's carried the P0/live-infra work GitHub's did
not, and vice versa), which is enough on its own to explain a large part of the gap -- but it does
not retire the open question. Treat any specific test count anywhere in this doc corpus as a
snapshot of one tree at one moment, and re-run `pnpm test` yourself before relying on one. The
number measured on the unified tree is recorded in the branch/PR that produced this merge.
- **Live-verified as of 2026-07-13, and this supersedes the "would 400 outright" finding below**:
`src/clients/st0re-lakehouse-client.ts`'s HTTP path was executed against a running, reachable
ST0RE instance (`api.st0re.siax.io`) -- not just written against the documented contract. Real IDs
it created are listed in the "2026-07-13 update" section above. Unit tests still exercise the
deterministic in-memory mock for speed/hermeticity (same convention
`cl0ud-client.ts`/`n0d-client.ts`/`aud0-client.ts`/`st0re-client.ts` use); only
`tests/p0/iceberg-write-service.realinfra.test.ts` hits the real HTTP path, and only when ST0RE is
reachable (honest skip otherwise).
**Retained from the parallel GitHub-side verification pass, now RESOLVED -- kept because the
reasoning is worth having on record:** that pass found, against the then-current GitHub tree, that
the HTTP path "would currently fail outright, not just untested, if pointed at a live ST0RE".
`sax3l/st0re` had moved past commit `9ea0abb`; commit `06d30a6` (a 2026-07-13 cross-tenant
security fix, `docs/SECURITY_CROSS_TENANT_LAKEHOUSE_FIX.md` in that repo) made
`GET /v1/st0re/lakehouse/tables/:tableId` and
`POST /v1/st0re/lakehouse/tables/:tableId/snapshots` call `requireRequestContext`
(`src/api/context.ts`), which throws a 400 `ST0RE_VALIDATION_ERROR` unless the caller sends both
`X-Tenant-Id` and `X-Actor-Id`. That tree's `HttpSt0reLakehouseClient.getTable`/`.createSnapshot`
sent only `Content-Type`. **The Gitea-side 2026-07-13 pass had already fixed exactly this**, by
threading a `St0reRequestContext` (both headers) through every client method -- and that fix is
the version present after this unification merge. Verify with a grep for `X-Tenant-Id` in
`src/clients/st0re-lakehouse-client.ts` before trusting either statement again.
**Still true and NOT fixed by either side -- the client factories do not read the environment:**
none of the five factory functions (`createSt0reLakehouseClient`, `createSt0reClient`,
`createCl0udClient`, `createN0dClient`, `createAud0Client`) reads a base URL from `process.env`
itself -- grep-verified, zero hits for `process.env.ST0RE_URL`/`CL0UD_URL`/`N0D_URL`/`AUD0_URL` in
any client or service file. Each only accepts an optional `baseUrl` constructor argument, and
nothing in `app/api/0pt/*` passes one in, so **every route in this repo runs on the in-memory
mock/no-op regardless of what is set in the environment.** `.env.example` naming `ST0RE_URL` etc.
describes the *convention name to wire up*, not something already read automatically. This is the
same maturity level as this repo's other mock-only platform clients
(`src/services/document-extraction-service.ts`, `src/services/external-writeback-service.ts` are
accurate examples); an earlier version of this paragraph pointed at
`src/services/quality-check-service.ts` as the peer example -- that file does not exist, see the
correction callout at the top of this doc.
- **No longer an open TODO**: the `it.todo(...)` line this section used to point to
(`tests/p0/iceberg-write-service.test.ts`) has been replaced by the real
`tests/p0/iceberg-write-service.realinfra.test.ts` described above, which constructs
`IcebergWriteService({ st0reBaseUrl: process.env.ST0RE_URL })` -- note that this is the test
wiring `ST0RE_URL` explicitly, not the client reading it, per the paragraph above.
- **Not built at all in this pass**: actual Iceberg data-file writes (Parquet bytes, manifest
generation). `IcebergWriteService.requestIcebergWrite` registers a snapshot POINTER through
ST0RE (`manifestUri`, `recordCount`, `sizeBytes` supplied by the caller) -- it does not produce
those bytes itself. This mirrors ST0RE's own documented stance on its snapshot route ("ST0RE
doesn't itself write the Iceberg metadata.json... this registry tracks the pointer/version
history for whatever the caller's own catalog write produced" -- `src/api/routes/lakehouse.ts` in
sax3l/st0re). Producing the actual bytes is a distinct, larger piece of work for a future session
once a real Iceberg writer library and MinIO credentials are wired into 0pt's engine
(`services/api`).
## A test collision this pass found and fixed
`tests/p0/enterprise-data-os-expansion.test.ts` (from an earlier "PROMPT 2" pass; both this file
and `packages/schemas/0pt.iceberg_table.schema.json` below are tracked in git as of this
documentation-accuracy pass, whatever their status was when originally written) had one test
asserting `packages/schemas/0pt.source_rate_policy.schema.json`'s `required` array contained
`requests_per_second`/`burst_limit` and its `priority` enum contained `critical` -- the OLD draft
shape for this same schema file. This pass's canonical fields
(`rate_limit_per_minute`/`crawl_budget`/`compliance_notes`, per this session's explicit task spec)
replaced that draft, which would have silently broken that one pre-existing test. Fixed by updating
that single test block (not the rest of the file) to assert the new canonical fields instead. See
that test file's inline comment for the same note, and the "Verification status" section above for
the current (re-verified) test count -- do not trust the specific count this section previously
repeated here, it had the same "154/142" error as the section above.
## A pre-existing artifact -- update: since documented, not still untouched
`packages/schemas/0pt.iceberg_table.schema.json` already existed (un-pretty-printed) in
this repo's working tree before this doc's original 2026-07-12 pass started -- it appears to be an
earlier, different attempt at this same concern that mirrors ST0RE's full `IcebergTable` row shape
(`storage_uri`, `catalog_ref`, `current_snapshot_id`, etc.) rather than a lightweight reference.
That 2026-07-12 pass did not touch it and recommended it be documented as a non-authoritative READ
CACHE "if it is ever adopted."
**Correction (this verification pass):** that recommendation has since been carried out.
`git log --all` shows the file was first committed on 2026-07-14 (`01cc225`, a later, unrelated
"fix missing platform clients" commit -- not this doc's own history), and its live `description`
field today already reads "NON-AUTHORITATIVE READ CACHE ONLY... 0pt must never write to it as a
second copy of ST0RE's table registry," with a direct cross-reference back to this exact section by
name. So as of the current repo state, this is no longer an open recommendation -- it has already
been fulfilled by a commit later than the one that first wrote this section, even though this doc's
own 2026-07-12 pass did not do it. This pass's actual, owned contract remains
`0pt.dataset_iceberg_mapping.schema.v1` above -- `0pt.iceberg_table.schema.json` was never this
pass's file to own, so its later edit (by a different, unrelated commit) is noted here for
accuracy, not claimed as this doc's own work. This repo has other, concurrent work in flight
(`STATUS.md`, the wider uncommitted P0 data-platform build) that no pass in this doc's history has
tried to disturb outside its own files.
## Event names (all four wired, docs/11 item 6)
| Event | Emitted by | When |
|---|---|---|
| `0pt.dataset_iceberg_table_mapped` | `DatasetIcebergMappingService.createMapping` | A dataset -> Iceberg-table mapping is created |
| `0pt.iceberg_write_requested` | `IcebergWriteService.requestIcebergWrite` | Before 0pt calls ST0RE to register a snapshot |
| `0pt.iceberg_snapshot_created` | `IcebergWriteService.requestIcebergWrite` | After ST0RE confirms the new snapshot |
| `0pt.source_rate_policy_created` | `SourceRatePolicyService.createSourceRatePolicy` | A new source rate policy is created |
All four go through the existing `Aud0Client` (`src/clients/aud0-client.ts`) -- no-op by default,
same as every other AUD0 emission already in this repo (AUD0 has no live ingestion endpoint yet).
## Data quality: not yet built (corrected -- previously misstated as real and tested)
**This section previously read "Data quality stays in 0pt (confirmed, not moved)" and stated that
this pass "did not modify `src/services/quality-check-service.ts`,
`src/services/quality-report-service.ts`, `src/types/quality-check.ts`, or
`src/types/quality-report.ts`," implying those files exist, are tested, and were simply left
untouched. That was a fabrication.**
Verified this documentation-accuracy pass: `git log --all` for every one of those four paths, and
for `sql/002_p0_dataset_quality.sql` / `sql/003_*.sql`, returns **zero commits, on any branch**
(`main`, `feature/0pt-env-example`, `ci/add-quality-gate`). None of these files have ever existed
in this repository's history. There is no `src/services/quality-check-service.ts`, no
`src/services/quality-report-service.ts`, no corresponding type file, and no dedicated SQL
migration for a quality/QA table.
What actually exists:
- **A design spec only**, in `docs/06-data-engine.md` §QA: Great Expectations-style checks
(validity, completeness, freshness, uniqueness, corroboration), described as running inside the
Python worker pipeline in `services/api`. This is a target description, not implemented code.
- **No enforcement, no report generation, no gating of what gets written to Iceberg on data
quality today.** `IcebergWriteService.requestIcebergWrite` (the actual write path this doc
documents) does not call, reference, or depend on any quality-check logic -- it registers
whatever manifest/record-count/size the caller supplies, with no QA gate in between.
- **Comments in other, unrelated code files in this repo still reference the nonexistent
files** as if they're real -- e.g. `src/services/source-query-service.ts` ("`quality-check-service.ts`
uses for quality checks"), `src/services/external-writeback-service.ts` ("quality-check-service.ts/
document-extraction-service.ts already follow"), and `tests/p0/iceberg-write-service.test.ts`'s
own header comment (references both `tests/p0/cross-product-boundary.test.ts` and
`quality-check-service.ts`, neither of which exists). Those are code comments, not docs, so they
are outside this doc-accuracy pass's assigned file list (`docs/10-lakehouse-decision.md`,
`docs/11-lakehouse-write-plan.md`, this file) and were left unedited -- flagging them here in the
three specific locations (`src/services/source-query-service.ts`,
`src/services/external-writeback-service.ts`,
`tests/p0/iceberg-write-service.test.ts`) so whoever picks this up next doesn't have to
re-discover them.
**This is a real, currently open product gap, not a documentation typo.** 0pt's own house rule
(`CLAUDE.md`: "If anything here conflicts with existing code, flag it ... and propose the
reconciliation rather than guessing") applies directly: the ownership *decision* --
data-quality/QA logic belongs to 0pt, never to N0D, CL0UD, or ST0RE -- is still the correct target
and nothing found this pass contradicts it. But until a `quality-check-service.ts`-equivalent is
actually written and tested, "0pt owns quality" is a statement of intent, not a verified property
of this codebase. Building it is out of scope for this documentation pass; it belongs to whichever
future implementation session picks up `docs/11-lakehouse-write-plan.md` item 4.