Turning published law into knowledge you can check.
Production-oriented AI engineering across legal knowledge
infrastructure, document intelligence, retrieval and data integrity. Five
services take a statute from the body that publishes it to a queryable
corpus that can prove what it contains — and refuse to serve what it
cannot. I implemented four of them.
Step 1 of 8Start with the landscape: two workstreams, one
platform.
Two workstreams, one platform
Both workstreams do the same fundamental thing: take a document
that somebody else authored, and turn it into knowledge a machine can query
without losing track of where it came from. One does it for German and EU
legislation, the other for insurance documents.
They share Kafka, PostgreSQL, MinIO, Qdrant and Neo4j, and they share no
topics, databases or collections. The Legal Knowledge Database is five
services and I implemented four of them; the document workstream is three
services I extended, one of which I started.
01The landscape. Colour is engineering role; border weight and the corner marker are authorship.
Open the SVG
4
services I implemented, out of 20 repositories inspected
14
verification gates, of which seven are required for the
aggregate badge
52
laws in the registry — 32 German, 20 EU; 25 certified as
structurally complete
Counts are read from app/data/law_registry.yaml and
audit/reports/corpus-certification-ledger.md as of 2026-09-12. The
service README states 31 and 51; the registry has moved since, and the registry
wins.
The one-paragraph version
A platform that does not only make authoritative information
searchable, but makes claims about that information falsifiable.
02Level 1. Four capabilities, no technology names.
Open the SVG
The Legal Knowledge Database
Five services in two groups — a three-stage ingestion
pipeline and a two-part control plane — over three Kafka topics and four
stores, with one rule that makes the whole thing verifiable: exactly one service
may write each store.
The ingestion pipeline is the entity producer, the preprocessor and the
loader. The control plane is the health service and the dashboard. I implemented
four of the five; the entity producer predates legal ingestion and I extended
it for EU sources.
That rule is not a convention. It was arrived at by removing a violation. An
earlier version of the loader deleted canonical rows, and it could never have
succeeded — currency_observations references laws with
ON DELETE RESTRICT and is append-only, so the delete was refused
every time, after Qdrant and Neo4j had already been cleared and committed. The
fix was to delete the code path and rename the replacement so the route states
its scope: DELETE /laws/{key}/projections.
03Level 3. Exact services, topics, routes and stores.
Open the SVG
The two boundaries
Reading this architecture wrong is easy in exactly one place, so it is
annotated three times in the source diagrams. law.structured means
canonical PostgreSQL is committed and nothing else — no vector exists, no graph
edge exists. law.embedded is the completion boundary: it is
published only after both derived stores converge under an exact count fence,
and it is the event that makes the preprocessor verify the law without anyone
asking it to.
One law, end to end
Eleven stages. Each one either produces evidence, or is measured
against evidence produced earlier.
04From publisher to queryable knowledge.
Open the SVG
Two orderings in that sequence are load-bearing. The independent source
inventory is built before the parser runs, so the denominator used to
check the extraction does not come from the thing being checked. And the
archive-mirror row is written last of all — after the object has been
uploaded, read back, hashed and size-checked — because that row is what the
raw-integrity gate treats as evidence. A row that exists has to mean bytes that
were verified.
05The conceptual spine. Derived knowledge is never the evidence for itself.
Open the SVG
Why four stores
Each store is here because the others cannot do its job. The
limitation row is the real justification.
06Four stores, four jobs, one writer each.
Open the SVG
How they stay in step without a distributed transaction
Every derived identifier is a pure function of PostgreSQL content. A Qdrant
point id is uuid5(namespace, chunk_id). A graph node is
MERGE-d by id, a law node by code. Identity is derived, never
generated — so re-running a delivery converges to the same state rather than
duplicating it. That single property is what replaces a transaction across a
relational store, a vector store and a graph store, and it is why a Kafka
redelivery is safe rather than dangerous.
Can we trust what is being served?
The system answers that with fourteen gates and four separate
statuses, and it deliberately refuses to answer it with one badge.
Seven gates are required for the aggregate the dashboard shows; the other
seven are recorded and displayed anyway, because a measurement nobody can see is
not a measurement. All fourteen apply to a German law. Twelve apply to an EU
law: fidelity.xml_byte_partition and
structure.source_profile are structurally inapplicable to FORMEX
and say so in code, with a named reason rather than silence.
07The short version: six mechanisms, each one there to stop the verification layer from confirming itself.
Open the SVG
08The full version: the gate inventory, how a verdict is produced, and the three operations that touch a law.
Open the SVG
What the gates cannot see
A count matching is not proof. A citation misread onto a provision that does
exist produces no orphan, moves no count, and fails no gate — which is why a
separate human review of citation targets sits alongside the machine gates, and
why COMPLETE is defined as machine structural assurance rather than
legal review. No law in the corpus claims to be human-certified.
09Fifteen documented failures, and the recovery that exists — or the statement that none does.
Open the SVG
The query path
Deliberately separate from the ingestion path. Nothing here
writes a store, and nothing here parses a law.
10Query time. The answering stage is an existing component whose repository is not in the inspected workspace.
Open the SVG
Retrieval is hybrid and single-round-trip: BGE-M3 produces a dense and a
sparse vector from one pass, both are prefetched, and Qdrant fuses them
server-side with reciprocal rank fusion. Dense catches meaning — a query for
“duty to disclose” reaches the German Anzeigepflicht.
Sparse catches exact tokens, which is what a citation like § 19
VVG actually is.
One refusal is worth more than the retrieval design. FlagEmbedding truncates
silently past the model's 8192-token window, so the loader counts tokens first
and raises for the whole law rather than storing a vector for text the model
never fully saw. A silently truncated embedding is undetectable afterwards: it
has the right dimensionality, sits in the right collection, and is wrong.
The operator control plane
Four actions, and what each one actually triggers behind the
button.
10Ingest, Verify, Reconcile, Withdraw — and the double gate on the two that change the corpus.
Open the SVG
12What a reader sees at each state of a re-ingest, including the window where the stores legitimately disagree.
Open the SVG
13The states a law occupies, reconstructed from code rather than from a generic lifecycle template.
Open the SVG
Withdrawing a law is not deleting it. The laws row survives,
withdrawn_at is set, and no MinIO object, raw document, publisher
capture, currency observation or verification check is touched by any step. A law
leaving the corpus does not make the record of what its publisher served
untrue.
Insurance document AI
A template is the extraction contract, not the model. That is
what lets the model change without the pipeline changing.
14From PDF to structured understanding, with the six document templates that define what structured means.
Open the SVG
The most portable piece of domain knowledge in that repository is a string:
"Look for: Policennummer, VS-Nr., Vertragsnummer, Police Nr.".
German insurance-document literacy, encoded as data, reviewable by someone who
knows insurance but not Python, and carried between models without retraining.
Multi-label chest X-ray classification
A research/prototype medical-imaging classification pipeline —
internally named “AI Radiologist”. Five findings, one calibrated
decision threshold each, and a Grad-CAM++ attribution map.
Scope: this is a prototype. It is not a medical
device, it has no regulatory clearance, it was not clinically validated or
deployed, and it does not assist or replace a clinician. The internal project
name is recorded here because that is what the repository calls it — it is not a
claim about clinical role.
Five findings — atelectasis, cardiomegaly, consolidation, edema and
effusion — each with an ICD-10 code and its own decision threshold. The
thresholds are Youden’s J optima from a validation set, which is why they
differ so widely: cardiomegaly is called positive at 0.130 and
effusion only at 0.424. A single global cut-off would have been
wrong for every class.
17The inference path, the five decision thresholds, and the one decision worth defending.
Open the SVG
A design decision, separated into fact and interpretation
What the code contains. A per-finding anatomical prior —
cardiomegaly centre-left, effusion lower, edema perihilar, each with a boost
factor. The mask is boost-only: it emphasises, it never suppresses. The flag
controlling it is USE_ANATOMICAL_HINTS = False, commented
“DISABLED — trust Grad-CAM fully”, and the surrounding comments
state that the model’s attention is the ground truth for localisation and
that the hints are soft only.
My reading of it, which the code does not state. An
engineering concern is that an anatomical prior could bias localisation towards
expected anatomy, so the attribution would tend to agree with the label rather
than show what the model actually used. That rationale is my interpretation of
the decision — the source documents “trust the model’s
attention”, not an argument about explanation bias.
I also read it as the same shape of decision as the legal corpus’s
independent oracle — a check that shares the assumption of the thing it checks
cannot falsify it. That parallel is my own framing across two codebases, not a
claim that either was built with the other in mind.
AI Compliance — what the corpus is for
The customer-facing assessment product. It is the reason the
Legal Knowledge Database has to be verifiable rather than merely searchable.
A customer works through a structured questionnaire; a headless browser
scans their site for evidence the questionnaire cannot ask for; a knowledge
processor reads the questionnaire graph together with the legal corpus and
produces a grounded assessment; the result is rendered to a PDF report behind a
short-lived token.
18The product, and the questionnaire bridge into the legal corpus. Authorship is marked per component.
Open the SVG
Why the corpus has to be verifiable
A compliance report tells an insurance customer whether they meet an
obligation, and cites the provision it relied on. If the corpus behind that
citation has drifted from what the publisher actually served, the report is
confidently wrong and nobody downstream can tell. The gates, the provenance
digests and the four separate trust answers exist so that this product’s
output is defensible.
Whose work this is: the assessment UI, the backend, the website
scan and the knowledge processor were built by colleagues and their
repositories are outside the workspace this portfolio was audited from, so no
internal detail is drawn. The questionnaire ingestion path is a
colleague’s feature living inside a service I implemented — I own the
loader, its Kafka handling and its embedding and graph-write layers; the
questionnaire service on top of them is marked Integration / shared
rather than claimed.
Ten topics, two pipelines, one broker
Exact topic names, their producers and their consumers — and the
places where the two pipelines differ in maturity.
19The topic inventory, plus the four delivery properties that were designed rather than inherited.
Open the SVG
What I built, extended and integrated
Commit shares are from git shortlog on 2026-09-12. A
commit count is evidence of weight, not of line ownership, so every specific
claim names a file or a commit.
20Grouped by engineering responsibility, with an explicit list of what I did not build.
Open the SVG
The four-level authorship encoding is consistent across every
diagram here: a 2px accent border and a filled corner marker for what I
implemented, a thinner border and a hollow marker for what I extended, a
hairline for existing platform components, and a dashed hairline for external
systems. Use the authorship control in the header to dim everything that is not
primary work.
Five problems worth describing
Derived from commits, audit records and code comments. Each one
has a constraint, a decision, a trade-off, and a consequence that was actually
observed.
A verification gate that was wrong in a way counting could not reveal
Problem. ZPO passed fidelity.source_substructure
cleanly and became the validation specimen for the evidence gates. BGB then
reported every structural path three times.
Constraint. The gate had to hold for any German law, and a
per-law exception would make the corpus unprovable as a whole: a rule that
fires for one key cannot be validated by any other law's evidence.
Decision. Treat it as a defect in the gate, not in the
data. ZPO's active version happens to carry a single evidence document; BGB
carries three — one current and two superseded — and the gate pooled them.
Trade-off. Fixing the gate invalidated the confidence
ZPO's pass had bought, and the rule became: audit each law's own structure
read-only before re-ingesting it, and treat a count that matches another law's
as a coincidence to verify rather than a baseline.
Consequence. Only running the gate against a second law
exposed it. One specimen is not a test suite.
Import sorting invalidated the provenance of the whole corpus
Problem. A lint pass sorted the imports in three files.
parser_rule_sha256 moved from 38eb6f833d0ab5ab to
b36ddd87ed592417, with no behaviour change at all, and every
stored generation's recorded ruleset stopped matching the installed one.
Constraint. The digest has to be exact to be worth
anything. A provenance mechanism that tolerates “harmless” edits cannot tell
you whether a stored law still reproduces.
Decision. Keep the digest byte-exact and disable
I001 for those three files, recording why in
pyproject.toml.
Trade-off. Three files are now exempt from a
project-wide lint rule, which is a real inconsistency, accepted because the
alternative weakens the only mechanism that can detect rule drift.
Consequence. The registry is part of the rule set too. A
code comment records the reason: adding one long form moved eight citations in
the AktG work.
Two stores, one snapshot, no distributed transaction
Problem. A law's vectors and its graph have to agree with
canonical PostgreSQL, and Kafka can redeliver any message at any time.
Constraint. No distributed transaction across a relational
store, a vector store and a graph store — the operational cost is not worth
paying for a corpus that is re-derivable.
Decision. Make every derived identifier a pure function of
PostgreSQL content, read the law from one REPEATABLE READ
snapshot, converge each store in a deliberate order, and fence each on an
exact count before publishing completion.
Trade-off. A real window exists between the canonical
commit and the projection fences where the stores disagree. It is short, it is
reported honestly by reconciliation as DRIFT_DETECTED, and
nothing serves a partial law while it is open.
Consequence. Redelivery became a non-event, which is what
made the absence of automatic retry survivable.
An endpoint that documented a measurement no code performed
Problem. The health service's validation endpoints
returned a job with status="completed" and the note “Live health
check completed against actual corpus stores”. The handler queried nothing.
Constraint. At the time there was no way for that service
to invoke the gates, which live in the preprocessor.
Decision. Delete the four endpoints and write down, in the
API reference, that the documentation had asserted a measurement no
implementation performed.
Trade-off. A visible capability was removed rather than
quietly left in place, which looks like regression until you read why.
Consequence.POST /laws/{key}/verify
replaced them once the gates could actually be invoked. A green result from a
check that never ran is worse than no check.
The running container was not the repository
Problem. Withdrawal was complete in the repository, the
docs and the tests — and un-exercisable against the running stack for every
law. The overlay images are built once and not rebuilt when files change, so
the preprocessor was missing a module entirely and the loader still served the
pre-rename route.
Constraint. An absent route returns 404, and the health
service maps a 404 from the canonical step to NOT_FOUND — “no
such law”. The symptom therefore reads as a data problem about the law you
named.
Decision. Verify the running image rather than the repo:
diff the container's files, or read the live route table from
/openapi.json, before concluding a feature works or is
broken.
Trade-off. Rebuild discipline is now a written ADR and a
boot-time check rather than a habit.
Consequence. The misdirection cost more than the bug. What
is running is not automatically what is written.
What I would keep, and what I would change
Keep
One writer per store. It turns “which service made
this row wrong?” into a question with exactly one answer.
Derive identity, never generate it. Idempotency for
free, across stores, with no coordination.
Separate the observer from the judge. The health
service records what the publisher served; the preprocessor's gate decides
what that means. Neither can quietly do the other's job.
Refuse rather than approximate. Blocking a law with
unrecognised structure, and refusing a law whose chunks would truncate,
both cost throughput and both prevent a silent corruption.
Keep four trust answers apart. One green badge would
hide which question failed.
Write the limitations into the artefact. Every
diagram in the source repositories states what it cannot promise.
Change
Add a dead-letter path to the legal pipeline. Today a
permanently unprocessable law is redelivered for ever and blocks its
partition. The document pipeline already has one.
Make the checker version impossible to forget. It
failed to move once when a gate's semantics changed, so verdicts either
side of that change are indistinguishable. Derive it from the gate
implementations rather than maintaining it by hand.
Content-address the captures. Immutable by key is not
immutable by policy: identical bytes submitted twice produce two objects,
and an orphaned capture is possible because MinIO is written before
Kafka.
Generate the gate list once. Three hand-maintained
copies exist downstream; that is recorded as a known defect and it is the
cause of the doc-versus-code drift found during this audit.
Build the FORMEX assurance contract. EU acts parse,
ingest and are queryable, and none can be certified, because the
pre-flight detectors are GII-shaped.
Re-own canonical deletion. A flag-gated purge path
still contradicts the single-writer rule. It is recorded as debt rather
than resolved by weakening the rule.
How this was assembled, and what it does not claim
Twenty repositories were read before anything was drawn. Every
claim on this site is traceable to a file, and the ones that are not are labelled
as unverified rather than smoothed over.
Verified in code
Service names, ports, topics, routes, gate identifiers, table and view
names, collection and bucket names — all read from source or
configuration.
Store ownership, count fences, identity derivation, delivery ordering
— read from the implementations.
Commit shares — git shortlog -sne per repository.
Corpus counts — the registry file and the generated certification
ledger.
Not claimed
No GDMS Rule Mapper. Asked for in the brief; no
repository, topic, consumer, class, configuration or document for it exists
anywhere in the workspace. Not drawn.
No production deployment. Only Docker Compose stacks
and CI image builds are evidenced. “Production-oriented” is the accurate
word.
No PaddleOCR, no DeepSeek-OCR, no Redis. None appears
in any dependency, configuration or source file.
No business impact metrics. None exists in any
repository, so none is stated.
No agentic RAG graph. A ten-node design exists in the
architecture material; langgraph is commented out and the
retrieval package is empty. Labelled design throughout.
Inconsistencies found between documentation and code
The health service's API reference states there is no delete endpoint. The
route exists at app/api.py:585, implemented by
app/withdrawal.py, and is documented correctly in the
architecture doc. The route was removed and later reintroduced; that section
was not updated. This site follows the code.
The preprocessor README states 31 German laws and 51 in total. The registry
holds 32 and 52 — DDG was added on 2026-09-03.
The gate reference marks seven gates as EU-applicable, while
families/formex.py implements four of the ones it excludes. The
same file states checker version :9 where the code says
:10. Its own header names the cause: three hand-maintained copies
of the gate list exist downstream.
The method and the headline findings are in
evidence-summary.md. The
full trail — ten documents naming exact file paths, per-repository
authorship and three documentation-versus-code discrepancies — is held
privately and available on request. No credential, internal hostname, endpoint
or client document appears anywhere in this published package.