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>
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, andsrc/types/quality-report.tswere real, tested files that this pass "did not modify." That was fabricated.git log --allfor every one of those paths, plussql/002_p0_dataset_quality.sqlandsql/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'sComputePolicy.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/st0redocs/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_atadded to0pt.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 barenamespacename string andcreated_at.namespace_idis populated from ST0RE's realLakehouseNamespace.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):- Every
/v1/st0re/lakehouse/*route requiresX-Tenant-Id+X-Actor-Idheaders -- the previousHttpSt0reLakehouseClientsent none. Fixed by threading aSt0reRequestContextthrough every client method. IcebergTableInput.datasetIdis a foreign key into ST0RE's own dataset table, not a free-form string -- the previous service sent 0pt'sdataset_idthere, which 400s live with a Postgres FK violation. Fixed by no longer sending it; the dataset<->table link lives entirely in 0pt's ownDatasetIcebergMapping.iceberg_table_id.- 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 newlistNamespaces).
- Every
-
Two ST0RE-side bugs flagged, not fixed (out of scope -- this task only touches the
0ptrepo):GET /v1/st0re/lakehouse/namespacesignores query filters and returns every tenant's rows regardless of the caller'sX-Tenant-Idheader;GET /v1/st0re/lakehouse/tables/:tableIdreturns a table's data even under a deliberately wrongX-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.tsreplaces theit.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(tenantt_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).
- namespace
-
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 --noEmithas one pre-existing, unrelated error insrc/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 testyourself 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 conventioncl0ud-client.ts/n0d-client.ts/aud0-client.ts/st0re-client.tsuse); onlytests/p0/iceberg-write-service.realinfra.test.tshits 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/st0rehad moved past commit9ea0abb; commit06d30a6(a 2026-07-13 cross-tenant security fix,docs/SECURITY_CROSS_TENANT_LAKEHOUSE_FIX.mdin that repo) madeGET /v1/st0re/lakehouse/tables/:tableIdandPOST /v1/st0re/lakehouse/tables/:tableId/snapshotscallrequireRequestContext(src/api/context.ts), which throws a 400ST0RE_VALIDATION_ERRORunless the caller sends bothX-Tenant-IdandX-Actor-Id. That tree'sHttpSt0reLakehouseClient.getTable/.createSnapshotsent onlyContent-Type. The Gitea-side 2026-07-13 pass had already fixed exactly this, by threading aSt0reRequestContext(both headers) through every client method -- and that fix is the version present after this unification merge. Verify with a grep forX-Tenant-Idinsrc/clients/st0re-lakehouse-client.tsbefore 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 fromprocess.envitself -- grep-verified, zero hits forprocess.env.ST0RE_URL/CL0UD_URL/N0D_URL/AUD0_URLin any client or service file. Each only accepts an optionalbaseUrlconstructor argument, and nothing inapp/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.examplenamingST0RE_URLetc. 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.tsare accurate examples); an earlier version of this paragraph pointed atsrc/services/quality-check-service.tsas 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 realtests/p0/iceberg-write-service.realinfra.test.tsdescribed above, which constructsIcebergWriteService({ st0reBaseUrl: process.env.ST0RE_URL })-- note that this is the test wiringST0RE_URLexplicitly, 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.requestIcebergWriteregisters a snapshot POINTER through ST0RE (manifestUri,recordCount,sizeBytessupplied 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.tsin 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 inservices/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.tsuses for quality checks"),src/services/external-writeback-service.ts("quality-check-service.ts/ document-extraction-service.ts already follow"), andtests/p0/iceberg-write-service.test.ts's own header comment (references bothtests/p0/cross-product-boundary.test.tsandquality-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.