Soilytix · Domain comic v0.3 · Sample lineage

The Life of a Soil Sample

A step-by-step story told from the sample's point of view — a low-level walkthrough of the canonical lineage, with every panel anchored to a domain aggregate, lifecycle rule, or invariant from the bounded contexts review. v0.3 adds a synthesis panel showing the full lineage tree shape.

Status  Working draft Source  Domain Model v1 (2026-04-13), Aggregate matrix, Boundary decisions Audience  Engineering, science, product
1 Before I existed: just dirt in a field Spatial context
AreaAsset · ManagementZone
"I'm a clump of soil somewhere on this field. I've been here for years. The field has a name and a boundary; I don't have either. I'm not yet a sample — just material in place."

The land exists before any sample, any order, any engagement. The field has a versioned boundary and may be split into management zones. None of that requires a customer.

Domain AreaAsset is a versioned spatial root, owned by an Organisation. ManagementZone lives inside an AreaAsset version (not an aggregate root). Neither belongs to an Engagement, SamplingCampaign, or InvestigationFrame.
2 Someone draws a plan over the field Commercial → Material
buffer N 9 planned sampling points · grid · 0.5/ha
"A scientist looks at a map of my field and decides where they'd like to take samples. I'm not picked yet — these are just intentions on a screen."

A SamplingCampaign is designed inside a ServiceSession. Sampling strategy, count, density, buffer zones, GPS export — all of this is planning, not collection. No physical sample exists yet.

Domain SamplingCampaign in design phase, owned by a ServiceSession inside an Engagement. References AreaAsset by stable identity (does not own it). May still complete with zero collected samples.
3 Two areas decide to pool Pooling strategy
Field A Field B Pool A 9 → 1 Pool B 11 → 1 SamplingPool · area-based · planned, not yet physical
"Before anyone goes outside, the team decides: all primary samples in Field A will be pooled into one composite analysis sample. Same for Field B. That's two pools planned, not 20 individual samples."

Pooling is a planning-time commercial decision before any soil is touched. Pricing is area-based, so a 5 ha field at 1ha resolution becomes 5 primary samples but only 1 composite analysis sample — and the customer pays for the composite, not the 5.

Domain SamplingPool is a planning artefact on SamplingCampaign / AreaAsset strategy. It declares "primary samples in this set will be physically combined into one analysis sample." A campaign may have many pools, or none — many sampling areas have no pools at all.
Distinction. The pool itself is a plan. The act of physically combining samples is a CollectionEvent. Two different things.
4 A kit with QR-coded bags is dispatched Campaign goes independent
SOILYTIX SMP-A-001 field code SMP-A-002 Sampling kit dispatched · campaign now in execution
"Bags with QR codes leave the office. Each bag is meant for a specific spot — but right now they're empty, and I'm still in the ground."

The "Request Sampling Kit" button transitions the campaign from session-owned design to an independent execution lifecycle. From this moment, even if the engagement is paused or cancelled, the campaign carries its own state.

Lifecycle split. Planning and execution of a campaign are different states; one cannot overwrite the other.
5 A field worker scoops me up Collection event
SMP-A-001 52.3°N 9.7°E 2026-05-07 09:14 CollectionEvent · FieldOrigin captured
"I get scooped into a bag and the QR is scanned. The system writes down where I came from, when, and from which campaign — but I still don't have a stable name."

A CollectionEvent is appended. It records the FieldOrigin: sampling point, campaign, AreaAsset version, coordinates, timestamp. Weather data is auto-pulled from nearby satellites.

Domain CollectionEvent is append-only. Corrections are made by superseding events, never by overwrite. Geo-located events reference an AreaAsset; material imports do not.
6 In the bag, I'm not alone Composite collection
POOL-A composite of 9 SMP-A-001…009 Field A 9 sub-samples mixed CollectionEvent · type=composite_creation 9 source primary samples consumed into 1 composite
"At each of nine spots in Field A, a small scoop is taken — but they all go into the same bag. By the time the bag is sealed, the nine sub-samples have lost their individual identities. They are now me — the composite."

Pooling at collection is a real, recorded act. The 9 primary sample identities (with their individual coordinates and timestamps) are preserved as inputs to one CollectionEvent of type composite_creation, and the composite has its own identity downstream.

Domain One CollectionEvent with N primary-sample inputs and 1 derived material output. The 9 underlying primary samples are never deleted — they remain queryable, with timestamps and coordinates intact. The composite carries forward.
Watch out. Don't lose the sub-sample provenance. The composite is one bag, but the model still has to remember "this composite was built from samples at these 9 coordinates" — that's required for spatial averaging, QC, and re-collection.
7 In a box on its way to the lab Provisional identity
SOILYTIX · cold chain CollectionEvent committed · no SampleLineage yet
"The system knows I exist and where I came from, but I haven't been given my real, durable identity. The label on my bag is a field code, not my domain ID."

This in-between state matters. The CollectionEvent is committed; provenance is captured. But until the lab physically receives the bag, no SampleLineage exists. A bag could be lost in transit and the campaign would still be valid.

Watch out. Don't conflate the field code on the QR sticker with the SampleLineage ID. The field code is provenance metadata; the lineage ID is the durable scientific root, assigned later.
8 Lab intake — I am given a name ★ SampleLineage stable ID
SMP-A-001 field code SL-X9F2-7K SampleLineage 2026-05-09 11:02 ReceivedSample → stable SampleLineage ID assigned
"At the lab bench I become SL-X9F2-7K. From this moment on, this is who I am. Even if my bag, the campaign, or the customer disappears — this name persists."

This is the single most important step in the whole story. The SampleLineage aggregate is born. It is the durable provenance root of everything that follows.

Aggregate invariant. SampleLineage does not require an Engagement to exist. It is not a child of Engagement, AssayOrder, or InvestigationFrame. Lineage can exist without any active commercial workflow.
9 How do I know which land I came from? ★ Cross-context reference
SPATIAL CONTEXT PROVENANCE CONTEXT COMMERCIAL CONTEXT AreaAsset Field A · v3 id: AA-7421 SamplingPoint 52.31°N 9.71°E id: SP-A-001 SampleLineage id: SL-X9F2-7K collectionEvents: [CE-44] CollectionEvent · CE-44 FieldOrigin { areaAssetId: AA-7421 samplingPointId: SP-A-001 campaignId: CAMP-19 } SamplingCampaign id: CAMP-19 references AA-7421 Engagement id: ENG-4421 commissioned CAMP-19 owns (same context) references by stable ID (cross-context)
"My lineage record doesn't contain the field. It just remembers an ID. The field lives in its own context — Spatial. To find out which land I came from, the system follows the reference."

This is the structural answer to your question: SampleLineage and AreaAsset live in different contexts and are never the same object. The bridge is the CollectionEvent, which carries areaAssetId as a value inside its FieldOrigin payload. That's a stable-ID reference — an association, not containment.

Reference rule. Cross-context links are associations, not hidden containment. Don't infer parent-child ownership from workflow adjacency or convenience joins.
Three resolutions Geo-located: CollectionEvent carries areaAssetId + coordinates. Lineage answers "where" via the reference.
Ad-hoc / non-geo: CollectionEvent carries a MaterialOrigin payload — no areaAssetId. Lineage exists, just without spatial provenance.
Land changed or dropped: AreaAsset is versioned, so the old areaAssetId@v still resolves. If the AreaAsset is deleted, the reference becomes a dangling ID — lineage doesn't break.
Two origin paths converge here
10 I'm not the only kind of sample arriving today FieldOrigin · MaterialOrigin
FieldOrigin samplingPoint · campaign areaAsset · coordinates MaterialOrigin supplier batch matrixType · supplier batchRef · description ReceivedSample stable SampleLineage ID assigned here Two typed origin paths · one identity
"On the next bench, a tube of biofertilizer from a partner lab is being checked in too. Different origin, same milestone — they get a stable lineage ID just like I did."

The model recognises two origin types — field-collected and material-imported — and they converge at the same moment: lab intake. After this, both look identical from the system's point of view.

Domain Two typed structs (FieldOrigin, MaterialOrigin) feed into a single ReceivedSample. No 1:1 field-sample-to-lab-sample mapping is required — composite, pooled, and transformed chains are valid.
11 My DNA is pulled out — I become a derivative ExtractionSample
SL-X9F2-7K received DNA extraction SL-X9F2-7K-E1 extracted DNA New derivative node · same lineage root
"My DNA gets pulled out. The original soil is mostly used up. The extract gets a new code, but it still descends from me — the lineage tree just grew a branch."

Extraction creates a new ExtractionSample node under the same SampleLineage. It's not a different sample — it's a derivative of the same provenance root. Multiple extractions from the same received sample are possible.

Domain New node in SampleLineage tree. The extraction is also recorded as an ExecutionAttempt with a MaterialConsumption value object — what was used, how much.
12 My extract is split for several assays AnalysisSample[]
SL-X9F2-7K-E1 …E1-A1 16S …E1-A2 ITS …E1-A3 chemistry 16S sequencer ITS pipeline fungal community chemistry N · P · pH · OM One ExtractionSample → many AnalysisSamples
"The extract gets split into three tubes — one for each assay. Each tube is its own analysis sample, but they're all branches of the same lineage tree."

An AnalysisSample is created per assay type. This is the structural reason a "sample" cannot be a single row in a table — it's a tree, and the tree's depth depends on how many assays were ordered.

Watch out. The legacy schema's primary_sample → analysis_sample as a flat M:N relation hides this tree. Don't backsolve the new model from that join table.
13 Pause: here is my whole family tree Lineage tree shape
Origin Root Extract Assay 9 sub-samples CollectionEvent CE-44 type=composite → FieldOrigin (panel 9) CollectionEvent CE-44b type=correction supersedes CE-44 1..n events ReceivedSample SL-X9F2-7K SampleLineage root stable identity consume 50g consume 50g (rerun) ExtractionSample …-E1 DNA · 1st extraction ExtractionSample …-E2 DNA · re-extracted 1..n A1 16S ✓ ok A2 ITS ✗ failed A3 chem ✓ ok A4 ITS ✓ rerun could exist 1..n SampleLineage = the whole tree under SL-X9F2-7K
"Step back for a second. This is everything that happened to me — nine sub-samples below, one root, two extractions (the second a re-do), four analysis samples, one of them failed and rerun. The whole tree is one lineage."

Up to here the story has been linear. The model is not. A SampleLineage is a tree, not a chain — and the cardinalities at every level are 1..n. Re-extractions, re-runs, additional assays, supersedeing CollectionEvents all branch the tree without breaking identity.

Cardinalities SampleLineage has 1..n CollectionEvents (corrections, top-ups, derivative creation), exactly one ReceivedSample root, 1..n ExtractionSample derivatives, 1..n AnalysisSamples per extraction. Each derivation step is recorded as an ExecutionAttempt with a MaterialConsumption value object — so the cost basis is in the tree, not bolted on later.
Schema implication. A "sample" cannot be a single row. It is a query over the tree rooted at SL-X9F2-7K. The tree is the persistence model.
14 The sequencer runs (and sometimes fails) ExecutionRun · ExecutionAttempt
ILLUMINA · MiSeq RUN-2026-127 attempt 1 of 1 Q30 = 91.2% execution ledger RUN-127 · attempt 1 SL-X9F2-7K-E1-A1 ✓ ok · Q30 91% RUN-128 · attempt 1 SL-X9F2-7K-E1-A2 ✗ low yield RUN-128 · attempt 2 re-run, internal QC ✓ ok · not billable RUN-129 · attempt 1 SL-X9F2-7K-E1-A3 ✓ chemistry ok Append-only · retries are new attempts, not overwrites
"The sequencer runs. The fungal assay fails the first time and is re-run. Both attempts are recorded — not just the successful one."

ExecutionRun is a first-class ledger primitive for any meaningful step: lab assay, bioinformatics pipeline, statistical compute. ExecutionAttempt distinguishes a fresh run from a retry, and tracks whether the retry is billable.

Lifecycle rule. Executions are append-only. Retries create new attempts; nothing is overwritten. Internal QC reruns and customer-caused reruns are distinguished here, not later.
15 Numbers fall out — raw measurements EvidenceSet
entity attribute value 16S_Abundance g__Sphingomonas 0.0342 16S_Abundance g__Bacillus 0.0218 ITS_Abundance g__Trichoderma 0.0091 Chem pH 6.4 Chem N_total 0.18 % Chem P_avail 14 mg/kg 1,229 rows avg per sample EvidenceSet · immutable scientific truth independent of engagement, order, report Raw, immutable measurements anchored to lineage
"At last — actual measurements about me. Thousands of rows: bacterial abundances, fungal counts, chemistry. They're locked once written. They are about me but they outlive me."

An EvidenceSet is a durable, immutable scientific aggregate. Produced either by an internal ExecutionRun or by an accepted ExternalEvidenceImport, with explicit attestation class.

Forbidden parentage. EvidenceSet is not a child of Engagement, AssayOrder, or ReportDeliverableVersion. It is its own root. Reports reference it; they do not own it.
16 Numbers become findings FindingSetVersion
Evidence pH 6.4 N 0.18% P 14 mg/kg Sphingo. 3.4% Tricho. 0.9% Benchmark corpus v2026.04 peer fields N=412 cereal · NW EU FindingSet v1 Primary constraint: low available P vs peers (corpus): P at 14% percentile N within range microbiome ok corpus v2026.04 pinned Evidence + benchmark → versioned interpretation
"Now I become a story. The numbers are compared against a peer corpus, contextualised, and ranked. "Low available phosphorus" is the headline."

Findings are an interpretation of evidence — produced by analysts (or the agentic harness) using approved methods and a pinned reference corpus. They are versioned independently and reissued when interpretation changes.

Domain FindingSetVersion is derived from EvidenceSet using a pinned CorpusVersion. Reinterpretation produces a new version. Old findings remain auditable.
17 Findings become recommendations Recommendation
FindingSet v1 low available P 14th percentile Recommendation → apply rock phosphate → trial mycorrhizal inoculant marketplace ref myco €38/ha P input €110/t cover crop Findings → recommendations → product references
"Once it's clear what's wrong, the system suggests what to do — and links those suggestions to actual products in the catalogue."

Recommendation is modelled separately from FindingSetVersion. Two interpretations of the same finding can produce different recommendations; both are tracked, and recommendations may reference marketplace items.

Domain Recommendation is its own concept — not a sub-property of a Finding. Its lifecycle (generated → reviewed → actioned) is provisional, awaiting product clarity.
18 A report is published to the customer ReportDeliverableVersion
Lineage SL durable scientific root Evidence EV immutable measurements Findings F v1 interpretation versioned SOIL HEALTH REPORT · v1 Customer: AgriCo · Field 7 Sample: SL-X9F2-7K · 1 of 9 Headline: low available P Pinned: EV-… · F-v1 · corpus 2026.04 commissioned by Engagement #4421 A view over truth — not the truth itself
"Finally — the customer sees a PDF with my name on it. But the report is just a presentation over the real things: lineage, evidence, findings. They live separately."

A ReportDeliverableVersion is commissioned by an Engagement and pins specific versions of evidence, findings, and corpus. Reissues are new versions; revocations are explicit acts.

Forbidden parentage. ReportDeliverableVersion does not own findings or evidence truth. The Engagement commissions; it does not own the science.
19 Long after I'm gone, my data persists Durability · auditability
T0 collected T1 lineage born T2 evidence locked T3 report v1 +3y re-analysis Physical sample fades · domain identity persists Lineage + Evidence + Findings remain queryable, versioned, auditable
"Three years later, a different scientist re-runs the analysis with a newer corpus. They produce FindingSet v2 — without ever needing me again. My evidence and lineage are enough."

This is why the structure matters. Lineage, evidence, and findings are separate, durable, versioned aggregates. Re-analysis, attestation, audit, and benchmark contribution all become possible without re-collection.

Why we model it this way. Material exists independent of requests. Requests operate on material; they don't own it. Evidence outlives requests. Deliverables ≠ execution outputs. (Infrastructure Invariants, Domain ground truth.)

What this comic encodes

If a single soil sample's life can be told as 19 panels, the underlying domain model has 19 corresponding decisions. The structural ones, in order:

Every "don't" in the boundary rules corresponds to a panel where a different design would have collapsed two distinct steps into one.