DRF-0134

Investigating P2 18h left of 24h Recurrence 7 of 30d

MIN-CNL-019 · Soy Candle Oud · at HQ_319 · stock type STOCK · detected 29-07 08:31 by shopify_level_divergence v4 · assigned to Ahmad Yousuf · back to queue

This is the seventh occurrence on this key in 30 days. Auto-accept is blocked for it, and incident INC-0109 owns the structural fix. The incident explains the case; it does not close it, and this case still needs its own posted entry. INC-0109 →

The two numbers that disagree

Each side names its authority, its observation time and how stale it is. Staleness decides which side is allowed to arbitrate at all.

frozen at detection
Side A · i3 ledger
560
Sum of quantity_delta over 412 entries since the signed opening balance. Last write 17-07 19:58, a GRN receipt of 40 units against PO-2026-118. Chain verified, balance_after = 560 on entry LEDG-0729-0091, cache agrees.
fresh · observed 29-07 08:31:04 · 0s stale
−10
AED 1,800
10 x 180.00
Side B · Shopify
550
Webhook inventory_levels/update received 08:31:22, available: 550, user_id: null, source: admin_manual_adjust. Shopify does not resolve the admin behind a public-token adjust, so there is no actor to ask.
fresh · observed 29-07 08:31:22 · 0s stale

Three-way read at detection

Frozen snapshot. This is what every later signature is bound to.

RFID cannot arbitrate
SourceValueObservedStalenessCan arbitrate?Why
i3 ledger56029-07 08:31:040sYesChain verified, every entry replayable from events_raw
Shopify55029-07 08:31:220sOnly for sellableShopify owns sellable, not physical existence. It cannot say where the 10 went.
RFID handheld55827-07 11:00:1249h 31mNoRead window is older than the disputed event. It bounds the truth, it does not settle it.
Balance cache56029-07 08:31:040sNot an authorityis_reconciled = true, last_balance_after = 560. A projection is never a witness.
Carton sumn/a--Not applicableMIN-CNL-019 lives in loose picking stock, not in master cartons

The RFID read of 558 sits between the two claims and is 49 hours old. It is consistent with the ledger to within normal read noise and it is consistent with nothing having physically moved. What it cannot do is confirm or deny a 10-unit change that happened this morning. i3 records that fact rather than quietly averaging the three numbers.

Last balance everyone agreed on: 600 units at 17-07 19:58. Net flow since: 40 sales, 0 transfers, 0 damages, 0 counts. Expected 560. Shopify is 10 below that with no ledger event to match.

What this case cannot do

It cannot close by agreeing with Shopify. Shopify owns sellable, and sellable in i3 is derived from custody and condition, never set directly. Writing 550 into the ledger to make the numbers match would be inventing an event.

It cannot close on the incident either. INC-0109 explains why manual adjusts keep happening. It does not move a single unit.

It closes exactly one way: a named explanation, a compensating entry that the approver signs, and a verification pass that re-reads the key afterwards.

Open
08:31:24 · auto · 0s
Triaging
08:44 · AY · 13m
Investigating
09:02 · AY · 5h 39m so far
Proposed
not reached
Awaiting approval
not reached
Resolved or accepted
SLA 29-07 08:31 + 24h

Evidence chain

6 rows · each carries a verdict, a weight, an observation time and a content digest. The digest is what a later signature is bound to, so an approval cannot be claimed to have covered different facts.

Matches
ledger_slice
560
412 entries · 17-07 19:58 to 29-07 07:10
Full replay of the key from the signed opening balance. Hash chain verifies. The last confirmed balance of 600 plus 40 sales minus 0 anything else lands exactly on 560.
weight 0.95 · sha256 4f2a9c1e…
Mismatch
webhook_payload
550
shopify · evt 20260729-0831-1049
Manual admin adjust with user_id: null. The classifier scored it 0.32 and routed it here rather than posting. No rule matched.
weight 0.80 · sha256 a71c0e82…
Stale
rfid_handheld_read
558
27-07 11:00 · RFD40 sled 3 · HQ_319 bay F
49h 31m stale. Within read noise of the ledger. Cannot arbitrate an event that happened 49 hours after it. Recorded so nobody later mistakes it for confirmation.
weight 0.25 · sha256 9d4a3c11…
Matches
sibling_state · grn
+40
GRN INF-2318 · 17-07 19:58 · CIC
The receipt that produced the last agreed balance of 600. Joined through grn_external_refs, not by parsing the GRN number.
weight 0.90 · sha256 c4e18f02…
Inconclusive
count_line
none
no count session on this key in 30d
HQ_319 bay F was last cycle counted 41 days ago against a 30-day policy. A recount is the cheapest way to make this case decidable, and it is one of the two proposed actions.
weight 0.10 · sha256 e02b7714…
Mismatch
derived_calc
−10
recurrence group RG-0031 · ordinal 7
Six prior cases on this exact key in 30 days, all admin_manual_adjust, totalling 31 units and AED 5,580. Every one of them was individually under the auto-accept cap.
weight 0.70 · sha256 118fa9d3…

Raw source payload

Exactly as received, before classification

evt 20260729-0831-1049
{ "topic": "inventory_levels/update", "received_at": "2026-07-29T08:31:22+04:00", "shop": "minimalistae.myshopify.com", "payload": { "inventory_item_id": 44827193411077, "location_id": 62839586885, "available": 550, "sku": "MIN-CNL-019", "user_id": null, "source": "admin_manual_adjust" }, "classifier": { "rule_matched": null, "confidence": 0.32, "routed_to": "drift", "posted_ledger_rows": 0 }, "resolution": { "sku_id": 1188, "via": "shopify_variant_id", "location_id": 3, "via_ref_table": "location_external_refs" } }

posted_ledger_rows: 0 is the important line. The classifier refused to post because no rule matched, which is the correct behaviour: an unexplained number is a case, not a ledger row. i2's ancestor of this path had no such refusal and is how sellable reached minus 140,753.

Ledger movements on this key

MIN-CNL-019 at HQ_319 · last 14 days

Full audit ↗
WhenTypeReferenceΔAfter
29-07 07:10sale#MIN10471−1560
28-07 16:22sale#MIN10462 · POS MCC−2561
27-07 11:00rfid_readRFD40-3 bay F0563
26-07 09:18sale#MIN10440−1563
25-07 14:55writeoffWO-0611 · MCC breakage−1564
24-07 18:30sale#MIN10418−2565
23-07 12:04sale#MIN10399 · POS YAS−1567
22-07 09:41sale#MIN10380−3568
21-07 17:12sale#MIN10362−2571
20-07 11:55sale#MIN10341−1573
19-07 14:20sale#MIN10322 · POS CCZ−4574
18-07 10:07sale#MIN10301−22578
17-07 19:58grn_receiptINF-2318 · PO-2026-118+40600
15-07 08:44sale#MIN10254−2560
Every row above has a balance_after. That column is what makes the cache checkable, and it does not exist in i2 at all: a repo-wide grep for it in the i2 schema returns zero hits.

Candidate explanations, ranked

From the closed explanation_catalog. Confidence ranks them; it never routes them. Exactly one may be chosen, and choosing it prefills the compensating entry.

#ExplanationConfidenceForAgainstWhat it says happenedWhat choosing it would postProof it needs
1 admin_manual_adjust 0.74 4 1 Somebody edited the Shopify level directly to reflect a physical fact i3 has never been told about. Six prior identical cases on this key. nothing yet. It says the number moved, not why the units did. Needs a second-level cause before it can post. the actor, which Shopify will not give us
2 confirmed_shrinkage 0.41 2 2 Ten units of wax product melted, broke or walked, and someone reduced Shopify instead of filing a damage. shrinkage · −10 @ HQ_319 · STOCK to WRITTEN_OFF · AED 1,800 · tier 2, one signature a recount, or a damage incident
3 unrecorded_transfer 0.22 1 3 Ten units went to MCC for the fragrance display without a transfer. MCC's own case DRF-0091 was 1 unit short on the same SKU four days ago. relocate pair · −10 @ HQ_319 and +10 @ MCC · movement_pair_id · net zero a matching overage at MCC, which does not exist
4 sample_withdrawal 0.11 1 3 Marketing pulled 10 units for a shoot and nobody raised a sample request. The Ceramic Atelier shoot was on 28-07. reclass · −10 STOCK, +10 SAMPLE_POOL @ HQ_319 · no value change a sample request, or the shoot manifest
5 detector_false_positive 0.04 0 4 The Shopify webhook is an echo of a write i3 itself made and the detector failed to suppress it. nothing. Voids the case and names the detector to fix. an outbox row within the echo window, of which there is none
Nothing is chosen yet, which is why the case is still investigating rather than proposed.A chosen explanation below 0.50 confidence requires a second signature regardless of value.

v2's detail page names hypotheses twice, in its empty state and in its error state, and renders none of them anywhere. That is the gap this block fills: without a closed vocabulary of explanations there is no root-cause Pareto on the queue page, no recurrence signature, and no way to tell a repeated bug from repeated bad luck.

Linked artefacts

Everything the leading hypotheses depend on, each a real record

KindReferenceRelationStateEffect on this case
IncidentINC-0109explainsInvestigatingPrefills the explanation. Cannot close this case.
Recurrence groupRG-0031member, ordinal 7HotBlocks auto-accept on this key
GRN receiptINF-2318last agreed balancePostedAnchors the expected 560
Purchase orderPO-2026-118source of the receiptReceivedCost layer for the AED 180 unit cost
Shopify eventevt-1049triggerUnclassifiedSide B of the disagreement
Count sessionnone in 30dwould decide itOverdue 41dRequested, not yet scheduled
Prior caseDRF-0091same SKU at MCCAcceptedClosed as confirmed shrinkage 4 days ago
Conflictnone-n/aNo two authorities claim the same fact here
Every cross-system reference above resolves through an external-ref table. i3 never parses a sibling's key, because there are seven live transfer state vocabularies and four incompatible reference formats between these systems.

Similar cases and how they closed

Same detector, same explanation family, last 180 days

7 prior
CaseDateKeyΔClosed asPosted
DRF-009125-07-2026MIN-CNL-019 · MCC−1confirmed_shrinkageWO-0611
DRF-007214-07-2026MIN-CNL-019 · HQ_319−4confirmed_shrinkageWO-0588
DRF-006102-07-2026MIN-CNL-019 · HQ_319−6confirmed_shrinkageWO-0552
DRF-004418-06-2026MIN-CNL-024 · HQ_319−3sample_withdrawalCOR-0731 reclass
DRF-003104-06-2026MIN-CNL-019 · HQ_319−8confirmed_shrinkageWO-0497
DRF-001921-05-2026MIN-CNL-019 · YAS−5confirmed_shrinkageWO-0461
DRF-000809-05-2026PP02 · HQ_319−2short_receiptCOR-0602
Five of seven closed as shrinkage on the same SKU in 90 days, worth AED 4,320. Accepting a sixth is not the cheap answer: at this rate the honest reading is that something upstream keeps removing units without telling i3, and the next accept should be blocked pending the incident.
"Apply the same resolution" prefills the form from DRF-0072. It never submits, and the approver still signs the digest of this case's evidence, not that one's.

Proposed compensating entry

Shown for hypothesis 2, confirmed_shrinkage. Nothing is written until it is signed, and nothing existing is modified when it is.

Open in the workbench ↗
correction COR-2026-0729-0034 state draft kind shrinkage sku_id 1188 display MIN-CNL-019 location_id 3 display HQ_319 from STOCK to WRITTEN_OFF qty_delta −10 value AED 1,800 (layer PO-2026-118, 180.00) drift_case DRF-0134 explanation confirmed_shrinkage root_cause theft_writeoff reason 248 chars, min 40 idem_key correction:COR-2026-0729-0034 -- dry run, computed 14:38:02, 41 ms -- correction_of_entry_id null nothing is being reversed; this posts a new fact movement_pair_id null no location changes, so no pair is required prev_hash sha256 c4e1…8f02 this_hash sha256 3f9e…41ab balance_before 560 balance_after 550 entries_rechained 0 always. this is a column with a CHECK, not a promise cache_writes 0 the projection follows the ledger, it is never written to

The v2 workbench reports entries_to_rechain: 4 here and says in its banner that a correction "re-chains all downstream entries". Recomputing downstream hashes is a rewrite of the chain, and a chain that gets rewritten in normal operation proves nothing. i3 appends at the tail and points backwards only.

Approval path

Tier from value and reversibility

Subject
ledger_correction
Tier
T2 one signature
Basis
value · AED 1,800
Reversible
Yes, by a further pair
Required role
role >= manager + drift_resolve
Requester
Ahmad Yousuf
May not sign
Ahmad Yousuf, as requester
SLA
24h from request
Digest
sha256 7b1e…c92f
RK
Ramesh Kumar · WH lead
signature 1 of 1 · not yet requested

If the value crossed AED 5,000 or the correction were irreversible, this would become T3 and need a Finance co-signature. Tier is computed from value and reversibility, never from the matcher's own confidence.

Why shrinkage and not "agree with Shopify"

Writing 550 into the ledger to match Shopify would be a number with no event behind it. i3 would then have a balance nobody can explain and a chain that verifies, which is the worst possible combination.

Posting −10 STOCK to WRITTEN_OFF says something specific and falsifiable: ten units existed, they no longer do, and here is who signed for the AED 1,800.

Post-resolution verification

Runs automatically after the entry posts. A failed verification reopens the case within one sweep interval.

not yet run
This case · pending
CheckExpected after postingResult
Ledger balance for the key550pending
Cache qty equals last_balance_aftertruepending
Cache is_reconciledtruepending
Divergence against Shopify0pending
Hash chain from the new tailverifiedpending
Case names its closing entrynot nullpending
DRF-0098, three days ago · passed
CheckExpectedResult
Ledger balance for the key8888
Cache qty equals last_balance_aftertruetrue
Cache is_reconciledtruetrue
Divergence against Shopify00
Hash chain from the new tailverifiedverified
Case names its closing entrynot nullLEDG-0726-0442

Verified 26-07 09:44, 34 minutes after the entry posted. This step is the whole difference between a case that is closed and a case that is fixed.

State timeline

Append-only. Nothing here can be overwritten by a later transition.

9 events
Case opened by detector08:31:24
shopify_level_divergence v4 · severity 2 from the value band · SLA frozen at 24h · recurrence group RG-0031 joined at ordinal 7
Auto-accept evaluated and refused08:31:25
Rule aar_micro_shrink_v1 declined on three of its guards: delta 10 over the 2-unit cap, value AED 1,800 over the AED 150 cap, and recurrence ordinal 7 at or above the block threshold of 3. Refusal is logged, not silent.
Alert raised08:31:26
alert.raise · drift.p1_opened deduped on (rule_id, dedupe_key, throttle_bucket). Module 01 owns delivery; this module never posts to Slack itself.
Assigned to Ahmad Yousuf08:44:02
Self-assigned from the queue · state moved to triaging
State changed to investigating09:02:47
Note: checking the receipt against the GRN and pulling the last handheld read
Evidence attached · rfid_handheld_read09:14:31
Verdict computed as stale automatically from 49h 31m against the detector's 24h freshness window. Weight dropped to 0.25.
Linked to incident INC-010911:20:08
Explanation prefilled from the incident's root cause. Case did not move to a terminal state, and INC-0109 cannot close while this case is open.
Recount requested for HQ_319 bay F13:05:44
Cycle count overdue by 11 days against a 30-day policy. Requested through module 10, session not yet scheduled.
Draft correction created14:38:02
COR-2026-0729-0034 · dry run computed · not submitted

Investigation log

4 comments · mentions notify through module 01

AY
Ahmad Yousuf29-07 09:06
Checked INF-2318. 40 units received 17-07 19:58 against PO-2026-118, all posted. No damages, no transfers, no counts since. The ledger's 560 is provable line by line, so this is not a ledger error.
RK
Ramesh Kumar29-07 10:22
Bay F was last handheld-scanned on the 27th and read 558. That is two below the ledger, which is normal noise for loose candle stock. It tells us nothing about a change made this morning.
FZ
Fatima Al-Zaabi29-07 11:15
This is the seventh time on this SKU since February and every previous one closed as shrinkage. I do not think it is shrinkage. Somebody is fixing Shopify by hand when the shelf does not match, and we only ever see the second half of that.
·
System linked recurrence group RG-0031 at ordinal 7 and requested incident INC-0109 · 29-07 11:20 · auto-accept remains blocked on this key until the group's 30-day count falls below 3
AY
Ahmad Yousuf29-07 13:07
Agreed with Fatima. Holding this at investigating until the bay F recount lands rather than accepting a sixth shrinkage. If the recount says 550 then it is real loss and we post it. If it says 560 then Shopify is wrong and the correction goes the other way.
Add a comment. Mentions with @ notify through the alert layer, never directly.
Comments are evidence of type human_statement at weight 0.30. They inform the ranking and they are never sufficient to close.

Available transitions

From investigating · everything else is refused by the state machine

ActionMoves toRequiresKeyboard
proposeda chosen explanation and a dry-run correctionP
blockeda named sibling and an expected unblock date; the SLA clock pausesB
investigatinga staff member with the feature flagE
investigatinga kind, an observation time and a digestA
proposedan open incident; prefills but never closesI
voidan explanation in category detector_defect plus a written note naming the detector to fixV
Mark resolvedresolvedRefused. There is no path from investigating to a terminal state that skips the correction and the signature.-
INV1 permits open to resolved directly with an optional note and no ledger row, and permits resolved to open, which sets resolved_by back to NULL and erases who signed. Both paths are absent here by construction.

The rule this page exists to enforce

A drift case has exactly four ways to end, and three of them post a ledger row. The fourth, void, names a detector defect and produces a ticket against the detector.

An incident explains a drift. It never closes it. INC-0109 will eventually change how Shopify admin adjusts are handled. It will not put ten candles back on the shelf, and it will not make the ledger agree with the world.

The converse holds too: INC-0109 cannot move to closed while this case is open. Otherwise the narrative gets tidied away and the stock consequence stays.

Where the incident's own remedy has already posted the units, this case closes by adoption: it points at that entry and writes no second row. A posted entry can be adopted by exactly one case, so two cases can never both close on one correction.

State variants

This page has no empty state: a case detail always has a case

Pattern library ↗
Loading

The disagreement pair holds its height, so the two numbers never swap positions as they land.

Error

Couldn't compute the variance.

The ledger replay for this key timed out. Evidence and the timeline are shown from cache; the proposed entry is hidden because a stale dry run must never be signed.

Back to queue
Permission

Read-only case.

You can see the evidence, the explanations and the proposed entry. Proposing or signing needs role >= manager plus the relevant feature.

Request access
Gate-blocked

Provisional case.

The detector that raised this is in shadow until GATE-1 closes. The case is real and visible; it is held out of the value-at-risk total and cannot be auto-accepted while the gate is open.

Connected to Drift queue Correction workbench Approvals Conflict inspector Anomaly explorer INC-0109 MIN-CNL-019 HQ_319 GRN INF-2318 PO-2026-118 Shopify events Classifier Stock count 3-way reconcile Unit events Damages Audit log Live inventory