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

23 KiB

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.