Investigations
Hypotheses, tested — not a paragraph of plausible prose
An explanation you cannot check is a guess with better formatting. Decim treats each possible cause as a claim to be settled by evidence, and shows its work either way.
Why an explanation is not a diagnosis
Ask any capable model why a pipeline lost rows and it will tell you. That is the problem.
You will get a fluent, well-organised answer listing schema drift, upstream delays, mapping gaps and connection failures. It will be correct in the sense that those are indeed the things that cause this. It will also be indistinguishable from the answer you would get about a pipeline that failed for a completely different reason, because it was produced without looking at yours.
The failure mode is specific: plausibility is not evidence, and prose has no mechanism for telling the two apart. A paragraph that says "the mapping table was likely stale" reads exactly like one that says "the mapping table was stale, here are the 455,382 rows it rejected and the timestamp of the first one." Only one of those survives someone checking.
So the unit of an investigation here is not a paragraph. It is a hypothesis — a specific claim, with a state, tested against something that ran against your systems.
A hypothesis is an object
Five states. The interesting ones are the two that never become an answer.
Each candidate cause carries a state, the evidence that moved it there, and a note on what it would take to settle it. The states are deliberately not a confidence gradient — they describe what the evidence did, which is a different thing from how sure anyone feels.
| State | Meaning | Why it exists |
|---|---|---|
| proposed | Generated, not yet tested | Exists so that the set considered is visible, not just the winner |
| supported | Evidence is consistent with it | Consistent is not sufficient — this state never becomes a diagnosis alone |
| refuted | Evidence contradicts it | Carries the artefact that killed it, so you can disagree with the reasoning |
| confirmed | Evidence is sufficient | Forms the diagnosis; must account for the observed magnitude |
| blocked | Cannot be tested | Names the evidence it would need — a request, not a shrug |
The distinction between supported and confirmed is the one that does the most work. A great many wrong diagnoses are hypotheses that were merely consistent with the symptom. Consistency is cheap — on any given day several true statements about your pipeline will be consistent with rows going missing. Sufficiency is the bar: the cause has to account for the size of what happened.
A worked investigation
Transaction volume down 38% overnight. Six hypotheses, four refuted, and an arithmetic check that nearly did not close.
What follows is the canonical demonstration incident, with illustrative figures. The shape is what matters: a symptom that could have half a dozen causes, most of which are eliminated cheaply, and one that survives because it accounts for the magnitude.
The symptom. A daily report shows transaction volume at 744,118 against an expected 1,200,000 — down 38%. Every run reported success. No alert fired. The scheduler log is clean, and the on-call engineer slept through the night, correctly, because nothing was broken in any sense a monitor understands.
- 1
H1 — Upstream sent fewer transactions
Refuted. The obvious first suspect, and the one that would make this somebody else's problem. The manifest shows 48 of 48 expected files received, every one within 2% of its 30-day median row count. Upstream sent a normal day.
Manifest against the trailing median -- H1: did upstream simply send less? -- Compare the day's manifest against the trailing median, per file. SELECT m.FileName, m.ReceivedAtUtc, m.RowCount, med.MedianRowCount, CAST(100.0 * m.RowCount / NULLIF(med.MedianRowCount, 0) AS decimal(5,1)) AS PctOfMedian FROM dbo.FileManifest AS m CROSS APPLY ( SELECT MedianRowCount = AVG(h.RowCount) FROM dbo.FileManifest AS h WHERE h.FileName = m.FileName AND h.ReceivedAtUtc >= DATEADD(day, -30, m.ReceivedAtUtc) AND h.ReceivedAtUtc < m.ReceivedAtUtc ) AS med WHERE CONVERT(date, m.ReceivedAtUtc) = '2026-08-05' ORDER BY PctOfMedian; - 2
H2 — A connection failure dropped batches
Refuted as the cause. There was a connection pool timeout at 02:07:19, and it did cost rows. But one batch is 500 rows against a shortfall of 455,882 — three orders of magnitude short. Kept on the record rather than discarded, because it is real and it still needs accounting for.
- 3
H3 — A schema change truncated or rejected rows
Refuted.
sys.objects.modify_dateputs the last alteration ofdbo.[Transaction]94 days before the incident, and no column type or nullability changed in the window. A schema change cannot cause a failure that began at 02:14 today.Object and column metadata -- H3: did the target schema change under us? -- modify_date on the object, plus current column metadata. If the table was -- last altered months ago, a schema change did not cause a failure today. SELECT o.name AS [table], o.modify_date AS table_last_altered, c.name AS [column], t.name AS data_type, c.max_length, c.is_nullable FROM sys.objects AS o JOIN sys.columns AS c ON c.object_id = o.object_id JOIN sys.types AS t ON t.user_type_id = c.user_type_id WHERE o.name = 'Transaction' AND o.type = 'U' ORDER BY c.column_id; - 4
H4 — Rows were diverted, not lost
Confirmed. The reject table holds 455,541 rows for the day, and 455,382 of them share one reason and one value:
unmapped PaymentType/CONTACTLESS. First seen 02:14:07, continuing to end of day. The remaining 159 are the ordinary background rate this table shows every day. The rows were never lost — they were routed somewhere nobody watches.Reject profile for the day -- H4: were rows diverted rather than lost? -- Group the reject table by reason and value. A reason that was absent last -- week and dominant today is the finding. SELECT Reason, RejectedValue, COUNT(*) AS Rows, MIN(RejectedAtUtc) AS FirstSeen, MAX(RejectedAtUtc) AS LastSeen FROM dbo.RejectedTransactions WHERE RejectedAtUtc >= '2026-08-05' AND RejectedAtUtc < '2026-08-06' GROUP BY Reason, RejectedValue ORDER BY Rows DESC; - 5
H5 — Something changed the set of payment types
Confirmed.
config/tender-map.jsonwas last modified 11 days before the incident and contains noCONTACTLESSkey. The upstream point-of-sale release notes for 04 August record contactless payments being reported as a distinct code where they had previously been folded intoCREDITCARD. The mapping did not change; what it had to map did. - 6
H6 — The nightly sync was responsible
Refuted as the cause, retained as a contributor. The 02:00 sync did exhaust the connection pool and did stall the loader for four minutes. It explains the 02:07 timeout in H2. It does not explain a shortfall that continues for twenty-two hours after the sync completed at 02:11.
The reject query returns the finding directly. One value, appearing for the first time at 02:14 and continuing all day:
Reason RejectedValue Rows FirstSeen LastSeen
------------------------ -------------- -------- -------------------- --------------------
unmapped PaymentType CONTACTLESS 455,382 2026-08-05 02:14:07 2026-08-05 23:58:41
unmapped PaymentType GIFTCARD_V2 118 2026-08-05 09:12:33 2026-08-05 20:44:02
failed FK: StoreId — 41 2026-08-05 06:03:19 2026-08-05 22:17:55 Where the arithmetic closes
The step that separates a diagnosis from a story: add it up, and see whether it comes to the right number.
A cause that explains the symptom qualitatively is not finished. The rows have to add up. Here is the first attempt, and it fails:
received 1,200,000 intake manifest, 48 of 48 files
loaded 744,118 dbo.[Transaction], batches 88101–88644
rejected 455,382 dbo.RejectedTransactions, reason 'unmapped PaymentType'
118 dbo.RejectedTransactions, reason 'unmapped PaymentType'
41 dbo.RejectedTransactions, reason 'failed FK: StoreId'
timed out 500 batch 88413, connection pool exhausted 02:07:19
---------
1,200,159 <-- does not close: 159 rows over 159 rows over. Small enough to wave away, and waving it away is exactly the move that turns an investigation into a narrative. The discrepancy is real and it has a cause: the loader's row-by-row retry path. When batch 88413 timed out, 159 of its 500 rows were retried successfully on the next pass and landed in the target — so they were counted once as timed out and once as loaded.
received 1,200,000
loaded 744,118
rejected 455,541 (455,382 + 118 + 41)
timed out 341 batch 88413 — 159 of the 500 were retried successfully
---------
1,200,000 <-- closes Why the rejected ones matter
Four of the six hypotheses above were refuted. That is not overhead — for most readers it is the more useful half.
Consider what the refutations cost to produce, and what they are worth. "Upstream sent less" is the first thing anyone checks, and checking it properly means finding a manifest, working out the right baseline, and comparing per file. When an investigation hands you that already done, with the query and the numbers, it has removed the most likely wrong turn before you took it.
There is a second, less obvious reason. A diagnosis with its rejections visible is falsifiable by the reader. You can look at H3 and say: that query only checks the target table, and our staging table has its own schema. Now you have found a real gap in the reasoning in ten seconds. A conclusion presented without its alternatives gives you nothing to push against except the conclusion itself, and disagreeing with a confident paragraph is much harder than disagreeing with a specific query.
This is also why refuted keeps its artefact rather than just a verdict. "We
ruled out a schema change" is an assertion. "We ruled out a schema change because
modify_date is 94 days old, here is the row" is something you can check and, if
warranted, overturn.
When it cannot tell
A blocked investigation names the evidence it needs. It does not fall back to the best available story.
The most dangerous moment in any investigation is the point where the evidence runs out but the pressure to conclude does not. A system optimised to always produce an answer will produce one, and it will be the most plausible remaining candidate — which is precisely the failure described at the top of this page, arriving by a more respectable route.
So blocked is a first-class outcome. It reports which hypotheses could not be settled and what would settle them, which converts a dead end into a specific, actionable request.
| Missing evidence | What cannot be established | Consequence |
|---|---|---|
| No intake or manifest table | Cannot establish a denominator | Reconciliation becomes relative, not absolute |
| Logs rotated before the window | Cannot correlate to a batch id | Retention silently sets the investigation horizon |
| No reject or error table | Cannot distinguish diverted from lost | The most consequential gap in this category |
| No source control connected | Cannot correlate to a change | Timing evidence remains; attribution does not |
| Reference table already overwritten | Cannot see what the pipeline actually read | A truncate-and-reload mapping destroys its own history |
Several of these are worth fixing before an incident rather than during one. A reject table with a reason column, a retained intake count, and log retention that outlives your reporting cycle are cheap to add and decide whether a future investigation is possible at all. The topology surfaces which of them you currently have.
Confidence, and what checks it
A number that is only meaningful if something eventually disagrees with it.
Each hypothesis and each diagnosis carries a confidence score. On its own that is close to worthless — a self-reported score is a claim about a claim, and a system with no feedback loop will happily report high confidence forever.
What makes it mean something is the comparison afterwards: what engineers actually confirmed when they closed the incident. A confidence score that is not calibrated against outcomes is decoration, and the honest position while the corpus is small is that calibration is a commitment rather than a track record. Decim is pre-release; there is no accuracy figure to quote here, and inventing one would undermine the argument the rest of this page is making.
Re-investigating
Evidence arrives late. A log shipper catches up, source control gets connected, someone restores a backup of the mapping table as it was that morning. Re-running an investigation against a fuller picture is normal, and a conclusion that does not survive more evidence was never a conclusion. Prior runs are retained so the two can be compared, which is also how the calibration above accumulates.
Common questions
Is this just an LLM summarising our logs?
What stops it from producing a confident wrong answer?
Can we see the hypotheses it rejected?
How long does an investigation take?
What happens when new evidence arrives?
Does it need to have seen the failure before?
Related: evidence for how each artefact is collected and cited; pipeline topology for how the graph is assembled; remediations for what happens after a diagnosis; and the write-up of this failure mode in longer form.
Get started
Bring us a pipeline that broke last week
The fastest way to evaluate this is a real incident you already know the answer to. If Decim gets it wrong, that is a far more useful demo than one where it doesn't.