* docs: full accuracy overhaul, grounded claim-by-claim in real code Every doc in this repo (CLAUDE.md, README.md, SECURITY.md, docs/*.md) was audited and rewritten by a multi-pass process: independent rewrite per cluster, adversarial re-verification of that rewrite against the actual code, then a final cross-doc consistency pass — each stage re-checking claims against the repo directly rather than trusting the previous stage. Two corrections matter most: 1. docs/ICEBERG_WRITE_BOUNDARY.md, docs/11-lakehouse-write-plan.md, and docs/P0_QUERY_SOURCE_FLOW.md asserted src/services/quality-check-service.ts, quality-report-service.ts, src/data-platform/services/catalog-service.ts, and sql/002_p0_dataset_quality.sql/sql/003_*.sql as real, tested code. git log --all across every branch shows zero commits for any of these paths, ever. 0pt's data-quality/QA enforcement does not exist in code. Corrected everywhere, with a new docs/P0_DATASET_QUALITY_FLOW.md added as an explicit target-spec (not-yet-built) replacement. 2. github.com/sax3l/0pt (this repo) and git.siax.io/sax3l/0pt (Gitea) are two diverged remotes sharing part of the same history (services/api/ workers/adaptive_router.py is byte-identical between them). Gitea's main independently accumulated a WAF-bypass/proxy-rotation scraper stack in three 2026-07-13 commits, since removed via a Gitea PR; GitHub's main never had it. Which remote is canonical/deployed is unresolved — flagged consistently across the corpus, not decided here. Also newly and consistently documented: the tenant_id/project_id-read-from- client-body gap on the P0 lakehouse/rate-policy/source-query routes (no auth/session system exists anywhere in this repo yet); that every CL0UD/ N0D/AUD0/ST0RE platform client is real but mock-only (zero process.env reads for any of their *_URL vars); that two P0 mutation routes (lakehouse/mappings, sources/rate-policies) always fail over HTTP today because their prerequisite entity can never be created via any route; a schema bug in sql/004 (FK references to opt_datasets/opt_sources, which don't exist); and that 0pt.app's live deployment was independently confirmed (curled this pass) to be running this exact codebase with DATABASE_URL/REDIS_URL unset in production. New file: docs/12-edge-capability-api.md — the edge-native P0 TypeScript capability contract (app/api/0pt/*, /api/screen, /api/ai/parse), previously undocumented and distinct from docs/04-api.md's FastAPI-engine-only scope. Verified before commit: tsc --noEmit clean; vitest run 88 passed, 1 todo, 0 failures (13 files) — docs-only change, no source touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs: reflect PR #5's lakehouse-mapping-registry + status-code fixes Follow-up to keep docs/12-edge-capability-api.md accurate once both PRs land: fix/0pt-full-bug-hunt (#5) fixed the shared-instance bug in the lakehouse mapping routes and added proper status-code mapping to the document extraction route. This doc was written against the pre-fix state in the same pass that added it, so it needs the same correction. Does not claim the lakehouse write path fully works now -- the deeper dataset-registration gap (no route ever calls DatasetService.registerDataset) is unchanged and still blocks it end-to-end; only the shared-instance half of the bug is fixed. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
18 KiB
03 · Data model
Postgres is the backbone. Two halves: data entities (what we sell) and platform tables (orders, users, jobs). Every fact links to bridge_provenance.
Migrations are plain SQL files in sql/, applied in filename order. There is no Alembic (or any other Python migration tool) anywhere in this repo, and no src/workers/db.py — that file does not exist (the Python engine's workers live in services/api/workers/ — adaptive_router.py, pipeline.py, queue.py, resolve.py, run.py — none of them named db.py, and there is no TypeScript src/workers/ directory either; src/ only has adapters/, clients/, services/, types/). infra/docker-compose.yml mounts ../sql as /docker-entrypoint-initdb.d on the postgres service, so Postgres auto-applies every .sql file in that directory, in name order, the first time that container initializes its data volume.
Only two migration files exist in this repo, ever: sql/001_init.sql and sql/004_iceberg_write_boundary.sql (verified via find sql/ — no 002 or 003). Three other docs in this repo (docs/ICEBERG_WRITE_BOUNDARY.md, docs/11-lakehouse-write-plan.md, docs/P0_QUERY_SOURCE_FLOW.md) mention sql/002_p0_dataset_quality.sql and sql/003_*.sql by name — they don't exist, on any branch, ever (git history checked) — but all three have been corrected to say exactly that (each carries its own "never existed" callout, none claims the files are real as of this pass). This file only asserts what's true of sql/001 and sql/004, which are real.
Conventions
snake_case, UTC timestamptz, soft-delete viadeleted_atwhere useful.- Natural keys for registries (
org_nr), surrogateuuidPKs for platform tables. created_at/updated_aton every table.- All monetary amounts in öre (integer) to avoid float errors; currency
SEKdefault.
Data entities
Mirrors sql/001_init.sql (verified against the file this pass; the migration additionally wraps every statement in CREATE TABLE IF NOT EXISTS for idempotent re-runs, and enables pgcrypto alongside vector — needed for gen_random_uuid()):
-- A company (the core registry entity)
CREATE TABLE dim_company (
org_nr text PRIMARY KEY, -- national reg. number (per market format)
market text NOT NULL, -- ISO country code: SE/NO/DK/FI/...
lei text, -- GLEIF cross-border key
name text NOT NULL,
status text, -- active/dissolved/...
nace text, -- NACE/SNI code
registered_at date,
address jsonb,
website text,
updated_at timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE fact_financials (
org_nr text REFERENCES dim_company(org_nr),
fiscal_year int NOT NULL,
revenue bigint, -- öre
result bigint,
employees int,
source_id text REFERENCES dim_source(source_id),
PRIMARY KEY (org_nr, fiscal_year)
);
CREATE TABLE dim_contact (
contact_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_nr text REFERENCES dim_company(org_nr),
name text,
role text,
email text,
phone text,
linkedin text,
-- PII: subject to GDPR; lawful basis recorded per delivery
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE dim_ubo ( -- ultimate beneficial owner
ubo_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_nr text REFERENCES dim_company(org_nr),
owner text,
share numeric(5,2)
);
CREATE TABLE dim_vehicle ( -- VehIQ synergy
vin text PRIMARY KEY,
market text NOT NULL,
reg_plate text,
make text, model text, year int,
owner_org text REFERENCES dim_company(org_nr),
specs jsonb,
updated_at timestamptz NOT NULL DEFAULT now()
);
Provenance model (the signature — every fact links here)
CREATE TABLE dim_source (
source_id text PRIMARY KEY, -- 'gleif','brreg','companies_house',...
name text NOT NULL,
market text, -- or 'global'
access text, -- 'api'|'bulk'|'authorized_web'|'licensed'
licence text, -- short licence note
cadence text, -- 'daily'|'weekly'|'annual'|...
url text
);
-- One row per (entity field) we deliver, linking value → where it came from + confidence
CREATE TABLE bridge_provenance (
field_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type text NOT NULL, -- 'company'|'contact'|'vehicle'|'financials'
entity_key text NOT NULL, -- org_nr / vin / contact_id
field_name text NOT NULL, -- 'revenue','email',...
source_id text REFERENCES dim_source(source_id),
freshness date, -- when this value was confirmed
confidence numeric(4,3) NOT NULL, -- 0.000–1.000
level smallint NOT NULL, -- 0 unavailable/licensed, 1 single, 2 partial, 3 corroborated
agreed_sources int DEFAULT 1, -- # independent sources that agree
UNIQUE (entity_type, entity_key, field_name)
);
CREATE INDEX ON bridge_provenance (entity_type, entity_key);
Rule: no value ships without a
bridge_provenancerow. Level 0 = delivered as "not available / licensed", never a guessed value.
dim_source is a different, older entity than the per-tenant Source registry described below (src/types/source.ts) that 0pt.query_source acts against — same name-ish concept, two separate schemas. src/types/source.ts's own doc comment calls this out, but its "~9-row list of national registries served today by GET /api/sources" phrasing is easy to misread: dim_source itself, as a Postgres table, has zero rows — sql/001_init.sql only creates the table, there is no seed/INSERT statement anywhere in this repo's SQL or Python (grep-verified for INSERT INTO dim_source). The actual 9-entry national-registry list that comment is pointing at lives in a third, distinct artifact: the edge-native GET /api/sources route (app/api/sources/route.ts), a hardcoded array with a confidence field. That's different again from the FastAPI engine's sources.yaml (8 entries, no confidence field — see docs/04-api.md's Sources section). So there are three separate "source list" artifacts in this repo — the empty dim_source SQL table, sources.yaml (8), and app/api/sources/route.ts (9) — none of which currently share data with each other; see "TypeScript-side P0 entities" below for the fourth, per-tenant Source type.
Platform tables
Mirrors sql/001_init.sql (verified this pass — same tables, same columns):
CREATE TABLE orgs (
org_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL, created_at timestamptz DEFAULT now()
);
CREATE TABLE users (
user_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
email text UNIQUE NOT NULL,
role text NOT NULL DEFAULT 'member', -- 'owner'|'admin'|'member'|'siax_admin'
created_at timestamptz DEFAULT now()
);
CREATE TABLE orders (
order_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
flow text NOT NULL, -- 'catalog'|'custom'|'subscription'
status text NOT NULL DEFAULT 'draft', -- see lifecycle in docs/01
config jsonb NOT NULL, -- configurator state (markets, fields, ...)
total_ore bigint, deposit_ore bigint, balance_ore bigint,
est_days int, est_confidence numeric(4,3),
created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now()
);
CREATE TABLE order_items ( -- resolved line items / dataset refs
item_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
kind text, ref text, qty int, meta jsonb
);
CREATE TABLE jobs ( -- one production run per order
job_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
stage text, -- sourcing|extraction|structuring|enrichment|qa
pct numeric(5,2) DEFAULT 0,
state text DEFAULT 'queued', -- queued|running|done|failed
metrics jsonb, -- rows, confidence, per-market progress
started_at timestamptz, finished_at timestamptz
);
CREATE TABLE deliveries (
delivery_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
formats text[], destinations text[],
record_count int, avg_confidence numeric(4,3),
quality_report jsonb, provenance_export_url text,
files jsonb, -- [{format,url,bytes}]
created_at timestamptz DEFAULT now()
);
CREATE TABLE subscriptions (
sub_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
cadence text, -- weekly|daily
webhook_url text, status text DEFAULT 'active',
next_run_at timestamptz
);
CREATE TABLE files ( -- customer uploads
file_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
order_id uuid REFERENCES orders(order_id),
name text, mime text, bytes bigint, r2_key text,
created_at timestamptz DEFAULT now()
);
CREATE TABLE db_grants ( -- customer read-only DB access (encrypted)
grant_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
order_id uuid REFERENCES orders(order_id),
conn_encrypted bytea NOT NULL, -- encrypted connection string
scope text, expires_at timestamptz, revoked_at timestamptz,
created_at timestamptz DEFAULT now()
);
CREATE TABLE audit_log (
id bigserial PRIMARY KEY,
actor text, action text, target text, meta jsonb,
at timestamptz DEFAULT now()
);
CREATE TABLE payments (
payment_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
kind text, -- 'deposit'|'balance'
amount_ore bigint, stripe_ref text,
status text, -- 'pending'|'captured'|'released'|'failed'
created_at timestamptz DEFAULT now()
);
Gap, verified this pass:
sql/001_init.sqldoes not containorder_items. It is documented here (and was already documented before this pass) as part of the target platform-table set, but there is noCREATE TABLE order_itemsin the migration file — check before relying on it existing in any real database.
Gap, verified this pass: this schema is real and (if
infra/docker-compose.yml'spostgresservice is actually run) gets created — but the FastAPI engine's own code never connects to it.services/api/orchestrator/main.pyhas noasyncpg/psycopg/sqlalchemyimport anywhere (grep-verified acrossservices/api/), and itsorders/jobsstate lives entirely in two module-level Python dicts (_ORDERS,_JOBS) that reset on every process restart and are never written to Postgres. The oneDATABASE_URLreference inmain.py(GET /health) only reports whether the env var is set — it never opens a connection. Seedocs/04-api.md's implementation-status section for the full picture. Until that wiring lands, this table set is schema-only, not a live system of record.
P0 registry tables (sql/004_iceberg_write_boundary.sql)
A second, newer migration adds two tables for the lakehouse write-path and crawl-compliance capabilities (0pt.write_iceberg_table, 0pt.source_rate_policy_manage — see the new edge-API doc, docs/12-edge-capability-api.md). Verified against the file this pass:
CREATE TABLE opt_dataset_iceberg_mappings (
dataset_iceberg_mapping_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
project_id TEXT NOT NULL,
dataset_id TEXT NOT NULL REFERENCES opt_datasets (dataset_id),
iceberg_table_id TEXT NOT NULL, -- reference into ST0RE's table registry, NOT a foreign key
namespace TEXT,
table_name TEXT,
mapping_status TEXT NOT NULL DEFAULT 'active',
mapped_by TEXT,
correlation_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ
);
CREATE TABLE opt_source_rate_policies (
source_rate_policy_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
project_id TEXT NOT NULL,
source_id TEXT NOT NULL REFERENCES opt_sources (source_id),
rate_limit_per_minute NUMERIC NOT NULL,
crawl_budget NUMERIC NOT NULL,
compliance_notes TEXT,
domain TEXT,
robots_ok BOOLEAN,
access TEXT,
status TEXT NOT NULL DEFAULT 'active',
correlation_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ,
created_by TEXT
);
iceberg_table_id is deliberately not a foreign key — it references ST0RE's own st0re_iceberg_tables.table_id, a different service's database. 0pt holds only the reference, never a copy of ST0RE's table/catalog/snapshot state (see the module doc comment on src/clients/st0re-lakehouse-client.ts, and docs/ICEBERG_WRITE_BOUNDARY.md for the full ownership boundary — that doc is outside this file's scope to otherwise correct).
Confirmed bug, found this pass — flagging plainly, not silently fixing it: both
CREATE TABLEstatements above have aREFERENCESclause pointing at tables that do not exist anywhere in this repo's SQL:opt_dataset_iceberg_mappings.dataset_id REFERENCES opt_datasets (dataset_id)andopt_source_rate_policies.source_id REFERENCES opt_sources (source_id). Neitheropt_datasetsnoropt_sourcesis created bysql/001_init.sql,sql/004_iceberg_write_boundary.sql, or any other file insql/(grep-verified — there are only two migration files, full stop). Ifsql/004_iceberg_write_boundary.sqlis ever actually run against a fresh database (e.g. viainfra/docker-compose.yml'sdocker-entrypoint-initdb.dmount), it will fail withrelation "opt_datasets" does not exist. This is not a naming coincidence:src/types/dataset.tsandsrc/types/source.tsdefine exactly theDataset/Sourceshapes those table names imply, andDatasetService/SourceService(src/services/) are their in-memory-only implementations — but no migration ever creates the SQL tables to back them. Open question for Simon: either author a migration that createsopt_datasets/opt_sourcesbeforesql/004(so the FK resolves), or drop those twoREFERENCESclauses and enforce the dataset/source-exists check purely in the TypeScript service layer (which already does the check today, redundantly, inDatasetIcebergMappingService.createMappingandSourceRatePolicyService.createSourceRatePolicy) — this file does not choose between those for you.
TypeScript-side P0 entities without SQL backing
Newer P0 capabilities (docs/12-edge-capability-api.md) introduced their own typed entities under src/types/, each with an in-memory-only service under src/services/. Only two of these (above) have a real migration. The rest have no SQL table at all — every instance of the owning service starts with an empty in-memory Map and loses all state on restart (or even between two new XService() calls in the same process — see docs/12-edge-capability-api.md's "in-memory isolation" findings for exactly how badly this bites in practice today):
| Type | File | Service (in-memory only) | SQL table |
|---|---|---|---|
Source |
src/types/source.ts |
src/services/source-service.ts |
none |
Dataset |
src/types/dataset.ts |
src/services/dataset-service.ts |
none |
DocumentRecord |
src/types/document.ts |
src/services/document-capture-service.ts |
none |
DocumentExtractionRecord |
src/types/document-extraction.ts |
src/services/document-extraction-service.ts |
none |
ExternalWriteRecord |
src/types/external-writeback.ts |
src/services/external-writeback-service.ts |
none |
DatasetIcebergMapping |
src/types/dataset-iceberg-mapping.ts |
src/services/dataset-iceberg-mapping-service.ts |
opt_dataset_iceberg_mappings (sql/004) |
SourceRatePolicy |
src/types/source-rate-policy.ts |
src/services/source-rate-policy-service.ts |
opt_source_rate_policies (sql/004) |
pgvector
CREATE EXTENSION vector; — this is real (sql/001_init.sql line 3, along with pgcrypto). Everything past the extension is target only: there is no embeddings table, or any other table using the vector type, anywhere in sql/001_init.sql or sql/004_iceberg_write_boundary.sql. The intended use (entity matching/dedup, AI-assist semantic match of NL request → catalog/config) is a real design goal, not yet a real table.
Row-level security & catalog
Target only — none of this exists in sql/ today, verified by grep for ROW LEVEL SECURITY, CREATE POLICY, and catalog across both migration files (no matches beyond a comment). There is no catalog table or view, and no Postgres RLS policy anywhere in this repo's SQL. Per-customer isolation is intended to be org_id filtering in the API plus RLS where practical — but as of this pass the FastAPI engine (services/api/orchestrator/main.py) has no org_id concept in any request or response at all (see docs/04-api.md's implementation-status section: GET /orders returns every order in the process, unfiltered). This is the same platform-wide "no auth/session system exists yet" gap noted across this doc corpus — not a data-model-specific problem to solve here, but the schema above cannot be considered tenant-isolated until it is.