18 KiB
06 · Data engine
The engine turns sources into delivered, provenance-tagged data. It is the legal motor — the whole product positioning depends on it. The real code lives in services/api/ (FastAPI orchestrator + workers), not at the src/workers/ path this doc used to point to — the "Pipeline stages" section below states plainly, stage by stage, what's built vs. still spec.
Known issue — two diverged git remotes for this repo (unresolved, Simon's call)
This doc describes the compliant-only sourcing chain as the law. That law was briefly violated on one of this repo's two remotes, so anyone reading this doc needs to know both exist:
github.com/sax3l/0pt(this session'sorigin) — the remote this documentation pass was verified against.git.siax.io/sax3l/0pt(Gitea) — a separate, diverged remote that shares origin history with GitHub for at least part of the codebase (services/api/workers/adaptive_router.pyis byte-for-byte identical between GitHub's currentmainand Gitea's post-fix PR branch, so they were once the same commit lineage).Gitea's
mainindependently accumulated three commits on 2026-07-13 that added a real WAF-bypass / proxy-rotation / fake-identity scraper stack (services/scrapers/*) — exactly the category of code §"Prohibited" below and CLAUDE.md §1 forbid. Those commits never touched GitHub'smain. The stack was since removed on Gitea via PR #1, but:
- Which remote is authoritative / deployed to
0pt.appis unknown — not resolved this session.- Whether the two remotes are supposed to be kept in sync is unknown — not resolved this session.
This pass does not resolve either question. Simon: decide which remote is canonical, confirm the other has no lingering bypass code, and either merge or retire the loser before treating "the 0pt repo" as singular in any future doc pass.
Sourcing chain (strict priority — this is the law)
For each field × market, acquire in this order; stop at the first that works:
- Official API / bulk — registries with programmatic/bulk access (highest confidence).
- Authorized public web — clearly public pages, respecting
robots.txtand rate limits, no auth walls. - Licensed partner data — contractually obtained.
- Otherwise → "not available / licensed only" — delivered honestly as level 0. Never guessed.
Current implementation covers tier 1 only. All five working connectors in services/api/sources/connectors.py
(gleif_by_lei, brreg_by_orgnr, companies_house_by_number, prh_by_businessid, cvr_by_number) call
official JSON APIs over httpx, nothing else. There is no HTML-scraping/parsing library anywhere in
services/api (no bs4/lxml/selectolax, no playwright, no scrapy/crawlee — checked against
requirements.txt and the source tree), so tier 2 (authorized public web) has no code path yet. Tier 3
(licensed partner) also has no connector — sources.yaml's bolagsverket entry is access: licensed with
no matching function in connectors.py; it correctly falls through to level 0 today. Both tiers remain
target design, not built.
Prohibited (do not build, keep, or extend)
- WAF/bot-detection bypass — including FlareSolverr, "stealth"/patched browsers, undetected-* used to defeat detection.
- Proxy/fingerprint rotation to evade bans; scraping behind login walls; automated fake-account creation (Apollo etc.).
- Reconciliation note (stale — kept for history): this section used to say "if
browser_worker.pyhas anHTTP → FlareSolverr → Playwrightchain, drop the FlareSolverr stage."browser_worker.pydoes not exist anywhere in this repository (verified this pass — no file of that name on GitHub'smain). There is nothing to reconcile in the current Python engine. The bypass code that did exist, briefly, lived inservices/scrapers/*on the Gitea remote only (see the Known Issue callout above) and has since been removed there via PR #1.adaptive_router.py(below) has never contained a bypass tier on either remote — it chooses among compliant methods only (API > bulk > authorized fetch > licensed), by design.
Pipeline stages (per job)
sourcing → extraction/OCR → structuring → enrichment/fusion → QA
The stage names below are the target architecture. services/api/workers/pipeline.py::run_job is the one
function that actually runs today, and it implements a much smaller slice of this than the stage list
implies — see the "built vs. not built" status called out under each stage below.
- Sourcing —
adaptive_router.plan_for_market(market)returns a hardcoded, ordered list of compliantsource_ids per market (see table below);pipeline.pytries each in order for a given key and stops at the first one that returns a result (breakafter first hit). This is sequential fallback, not concurrent racing — the phrase "source-racing = query several authorized sources and take the fastest/freshest" describes a target behavior that isn't implemented; today only one source is ever queried per key unless the first fails. Writes raw payload fields directly into the record dict (no separate raw-payload store). - Extraction / OCR — not built. There is no
ocr_worker, nounstructured, nopaddleocr/pytesseractanywhere in this repo (checkedservices/api/requirements.txtand the full source tree — none present). "Extraction" today is just each connector inconnectors.pyparsing the JSON response of its own API call into aCompanyResult. Document/PDF/image ingestion and the OCR cascade described here are spec only. - Structuring — partially built.
services/api/workers/resolve.py::resolve()does real, deterministic entity resolution: block by(market, name[:1].upper()), thenrapidfuzz.fuzz.token_sort_ratio(...) >= 92to merge duplicates, bumpingconfidenceby +0.1 per corroborating match (capped 0.99) and incrementingagreed_sources. That part is real and unit-testable. What's not built: no spaCy/transformers, no NER, no multilingual normalization (sv/no/da/fi/de/frfrom the original text), and no explicit LEI-based cross-market entity linking — matching is name-string-similarity only, within the same market.spacyandtransformersare not inrequirements.txt. - Enrichment / fusion — partially built, not fully wired into the job. The confidence/agreement bump
described above happens inside
resolve(). Separately,opensanctions_screen()(sanctions/PEP) andrdw_vehicle()(NL vehicle data) are real, working connectors inconnectors.py— butpipeline.pynever imports or calls either one. They're only reachable as their own standalone endpoints (GET /screen,GET /vehicles/{plate}inorchestrator/main.py), not as an automatic enrichment step on an order's records. NACE/SNI classification and "website/tech" enrichment have no code anywhere. - QA — not built beyond a single threshold filter. The entire QA stage in
pipeline.pyis:delivered = [r for r in deduped if r["confidence"] >= threshold]. That's real and does drop below-threshold rows (deliver fewer, never fabricate — that principle holds). But there is no Great Expectations (not inrequirements.txt, not imported anywhere), no separate validity/format, completeness, freshness, or post-dedup-uniqueness checks beyond the confidence number already computed in structuring, and no quality-report object emitted or stored anywhere —deliveries(the SQL table defined insql/001_init.sql) is never written to by this service. This mirrors a confirmed, broader finding this session: 0pt's data-quality/QA layer was at one point documented elsewhere (TypeScript side,src/services/quality-check-service.tsetc.) as if built and tested, but those files do not exist in this repo's history at all (zero commits, any branch — seedocs/07-dependencies.mdnote).docs/ICEBERG_WRITE_BOUNDARY.md,docs/11-lakehouse-write-plan.md, anddocs/P0_QUERY_SOURCE_FLOW.mdhave all since been corrected — each now carries its own explicit "fabricated, not built" callout rather than asserting those files as real — anddocs/P0_DATASET_QUALITY_FLOW.mddocuments this gap as target-spec-only. The Python engine independently confirms the same gap: no real quality-check enforcement exists in this codebase today, in either language.
"Each stage updates jobs ... so the production tracker / SSE can render it live" is not what happens.
run_job runs synchronously start-to-finish and returns one final dict (stage: "qa", pct: 100, state: "done", metrics, records). orchestrator/main.py's POST /orders/{oid}/run awaits the whole thing and stores that one
result in an in-memory _JOBS dict; GET /orders/{oid}/status just returns whatever's there, or a static
{"stage": "queued", "pct": 0, "state": "queued"} placeholder before it's run. There is no per-stage
incremental progress, no pct that climbs, and no SSE endpoint exists anywhere in orchestrator/main.py
(grepped — none). Live progress rendering is target design, not current behavior.
adaptive_router — actual market coverage (verified against services/api/workers/adaptive_router.py)
MARKET_SOURCES currently has real entries for exactly five markets, each a short compliant list — not the
"elaborate multi-source lists" an earlier version of this router may have implied:
| Market | Ordered plan | Note |
|---|---|---|
| NO | brreg, gleif |
|
| GB | companies_house, gleif |
|
| FI | prh, gleif |
|
| DK | cvr, gleif |
|
| SE | gleif only |
Bolagsverket bulk is licensed (see above); SE company core relies on GLEIF + a not-yet-built licensed/partner route |
| anything else | ["gleif"] |
fallback in plan_for_market() |
ACCESS_RANK (api=0, bulk=1, authorized_web=2, licensed=3) is defined in the same file but is not
actually used by plan_for_market() to sort anything — the lists above are hand-ordered constants already
in the desired priority, and the rank dict is currently dead code (documentation-as-code, not logic).
Confidence & provenance scoring
- Per field,
connectors.py'sProvenancedataclass carries:field_name,source_id,confidence(0–1),level(0–3),freshness.agreed_sourcesis not a field onProvenance— it's tracked separately, at the merged-record level, byresolve()(match["agreed_sources"] = ... + 1). If a doc or downstream consumer expectsagreed_sourcesinside eachbridge_provenancerow, that's a mismatch to reconcile against the actual dataclass shape, not a fact to assume. - Level: 0 = unavailable/licensed; 1 = single source; 2 = partial corroboration; 3 = ≥2 independent sources agree.
est_confidenceon the order = aggregate target (computed inorchestrator/main.py::compute_estimate); deliveredavg_confidence= actual, computed inpipeline.pyas the mean confidence of delivered rows.- None of this is persisted to Postgres by the current service.
sql/001_init.sqldefines thebridge_provenancetable (plusdim_company,dim_source,fact_financials,deliveries, etc.), andasyncpgis a listed dependency — but nothing inservices/apiactually callsasyncpgor issues a query.DATABASE_URLis read exactly once, inorchestrator/main.py's/healthhandler, purely to report a boolean ("db": bool(os.getenv("DATABASE_URL"))).orchestrator/main.pystores orders and jobs in in-memory dicts (_ORDERS,_JOBS) that reset on every restart. The schema exists; the wiring from pipeline output → Postgres rows does not yet.
Network-layer provenance: 1p (not yet in use)
bridge_provenance answers which source a field came from. It does not answer what
actually happened on the wire — which policy authorized the fetch, whether robots.txt was
read and what it said, or whether the bytes we parsed were the page or a bot wall served with
HTTP 200. 1p, the SIAX egress measurement point, closes
that gap and extends the provenance chain all the way down to the network layer.
1p owns four things: a per-destination policy as code (default DENY; rate, robots/ToS stance, allowed methods), a single egress choke point, block detection, and one provenance record per fetch written to AUD0. Everything else is composed (AUD0, ST0RE, CL0UD).
Why this matters for 0pt specifically — three facts that are hard to assert without it:
| Claim 0pt makes | What 1p adds |
|---|---|
"acquired from an authorized public page, respecting robots.txt" |
The record carries the robots decision plus a sha256 of the robots.txt it was taken from. A policy that says respect and cannot read robots denies the fetch — silence is not permission. |
| "these are the bytes we parsed" | body_sha256 per fetch, so a delivered field can be tied back to the exact response at the exact time. |
| "level 1/2/3 corroboration from independent sources" | Responses are classified ok / rate_limited / forbidden / captcha / blocked_soft / error. blocked_soft is the expensive one: HTTP 200 whose body is a block page. Without it, a bot wall gets parsed and scored as corroborating data. |
It is a measurement point, not a bypass. This is consistent with, not an exception to,
the prohibitions above and in CLAUDE.md §1: 1p exists to detect blocking and prove
provenance, never to evade. Its purchased-residential egress pool is an interface with a stub
that throws — wiring it up would be a separate, explicit decision. Block detection also
alarms on a rising trend, never on single events, and deliberately excludes error
(their 5xx, our timeouts) from the blocking measure.
Status: not in use. 1p is Phase 0 — code, contracts, tests; not deployed, no credentials
set. No 0pt worker routes through it today, and bridge_provenance remains the only
provenance 0pt writes. Adopting it is a later, separate step.
sources.yaml (machine-readable registry)
Single registry that drives the router, the /sources API, and the (still-to-be-built) public sourcing-map
page. services/api/sources/sources.yaml currently has 8 entries: gleif, brreg, companies_house,
prh, cvr, bolagsverket, rdw, opensanctions. Schema per entry (as actually used, confirmed against the
file — no entities/fields mismatch found):
- source_id: gleif
name: GLEIF (LEI)
market: global
access: api # api | bulk | authorized_web | licensed
cadence: daily
licence: "open (CC0) — VERIFY current terms"
entities: [company]
fields: [lei, legal_name, address, status]
base_url: https://api.gleif.org
robots_ok: true # for authorized_web only; must be honored
notes: "cross-border key"
orchestrator/main.py loads this file once at import time (yaml.safe_load(...)) and serves it through
GET /sources (id/name/market/access/licence/cadence only) and GET /sources/{market}. It is genuinely the
live registry behind those two endpoints — that part of the doc's original claim holds.
Starter set (VERIFY current access/licence per source before relying — terms change)
- GLEIF / LEI — typically open; cross-border key. Connector:
gleif_by_lei— real, working. - Brønnøysund (Brreg, NO) — typically open API. Connector:
brreg_by_orgnr— real, working. - Companies House (UK) — free API (key required,
COMPANIES_HOUSE_API_KEY). Connector:companies_house_by_number— real; degrades honestly to level-0 unavailable if the key is unset (does not fabricate). - PRH (FI) — Connector:
prh_by_businessid— real, working. - CVR / Virk (DK) — connector actually calls
cvrapi.dk(a third-party aggregator), not Virk's own API directly, despite the registry entry'sbase_url: https://datacvr.virk.dk.sources.yaml'sbase_urlforcvrdoes not match whatconnectors.py::cvr_by_numberactually calls — flag for reconciliation (either updatebase_urltocvrapi.dkor switch the connector to call Virk directly; not resolved this pass). - Bolagsverket (SE) — restricted for bulk;
sources.yamlmarks itaccess: licensed; no connector exists for it inconnectors.py. Consistent with the doc's own "plan licensed/partner route" framing — not a false claim, just confirming it's still unbuilt. - RDW (NL) vehicle data — open, Socrata-backed. Connector:
rdw_vehicle— real, working, but not called by the job pipeline (only reachable viaGET /vehicles/{plate}). - OpenSanctions — global sanctions/PEP screening. Connector:
opensanctions_screen— real, working, same caveat: reachable only viaGET /screen, not part ofrun_job. - Vehicle beyond RDW (Traficom FI, Transportstyrelsen SE, DVLA UK) — no connectors exist yet; (verify each before building; vehicle/PII rules differ).
Do not hardcode access terms as fact. When building each connector, confirm the current API/licence/robots status and record it in
sources.yaml. If unclear or disallowed for bulk, route to licensed/partner or mark fields level 0.
Structured financials (XBRL) — real but unwired
services/api/sources/financials.py::companies_house_filings() is a real, working function: given a
Companies House number and COMPANIES_HOUSE_API_KEY, it fetches the accounts filing history and returns up to
10 filings. It is not imported or called anywhere else in the codebase (grepped services/api — only
financials.py itself references the function name). The file's closing comment describes a further XBRL
extraction step ("with arelle installed, download each accounts filing's iXBRL document and pull tagged
facts...") that has no code at all — arelle is not in requirements.txt. Treat the whole file as a real but
disconnected building block, not a wired pipeline stage.
Libraries used here
Target list only — see docs/07-dependencies.md for what's actually installed today vs. still to be added,
with an explicit install-status callout per package. As of this pass, the only Python packages installed for
this engine are fastapi, uvicorn, pydantic, httpx, asyncpg (unused), python-dotenv, pyyaml,
rapidfuzz, tenacity — i.e. the API-only tier 1 sourcing + structuring-by-fuzzy-match slice described above,
nothing from the extraction/OCR/NER/quality layers this section used to list as if already in place.