Docket MVP: Reading Records Without Leaking Them
If your agent holds the divorce filing in context, it's a leakage risk. Privacy kills most legal-AI demos before they ship. Docket MVP is what the surface looks like when privacy stops being a feature flag and starts being a build constraint.
Privacy has killed more legal-AI demos than any single architectural choice. A demo that survives a real pilot goes through the door of a real office — real names, real divorce filings, real estate records — and an operator who is not going to dump any of those into a model and pray. Pilots that fail at that hurdle die in the room and are never heard from again. The buyers at the front of the market — general counsel, compliance leads — know this better than the designers.
Docket MVP is our build of the surface that doesn't die there. The architecture has three named pieces: MaskGuard as the wire guarantee, enrichment drip as the operational shape, and the operator workflow that never trades compliance for convenience.
What Docket MVP actually does
Docket is DSC's product for docket-and-matter-facing operator work with AI assist under privacy constraints. The operator lives in cases, contacts, dockets, and document inventories. The assist layer underneath — proposal drafts, entity resolution, contact enrichment — can do useful work, but only if the wire underneath it never lets a masked or partial value through to where the model reads it.
The MVP we shipped in early July 2026 is the smallest surface that proves the privacy constraint builds correctly. There is no "trust the lookup, decide later" path; bulk-import paths require opt-in review and run through the same wire. Each piece lives inside the guarantee, not adjacent to it.
If you operate in legal, the rest of this piece is the part you came for. If you don't, the punchline is: the wire enforces the same rules whether the developer watching the screen is paying attention or not.
MaskGuard is a build constraint, not a feature
MaskGuard is the wire-level rule that decides whether an enrichment fact is allowed into the operator's contact record. The rule, stated plainly: a record comes back from an upstream enrichment lookup either with full values or with no values at all. If the upstream reply contains the asterisks (B****), the asterisks do not pass the wire. They get rejected. The contact row gets a no-go flag, not a partially-masked row.
That is a hard rule no one turns off, because it is what a privacy constraint produces when it produces the right system. The competitor's instinct at this layer is "let the operator decide" — pass the masked value through, surface a UI warning, let the operator make the call. We've watched that pattern die in pilots. The operator reads the warning, makes the call, and the leak surface still exists for the duration of the product's life. MaskGuard removes the call from the wire entirely.
Where the engineering discipline shows up: a strict labelMeansMasked() function, not a substring match. We had a stall bug earlier in 2026 where a label like bv_unmasked — the opposite of a masked record — tripped a naive substring search for "masked." The bug stalled the pipeline until we caught it, and you can see why in retrospect. The fix is an explicit assertion: MaskedEnrichmentRejectTest.php regresses labelMeansMasked('bv_unmasked') === false. The test exists so that bug doesn't come back, and the heuristic of the product is to add a regression test for every wire-level rule we ever break. We didn't reach that heuristic on the first try.
Enrichment drip is the operational shape
The other half of the architecture is enrichment drip — incremental enrichment via a claim-and-heartbeat work pattern, instead of one-shot bulk scrapes.
A contact record becomes enriched through Docket the way a clinical procedure looks. The ContactEnrichmentStore holds the queue. A worker claims a record. It runs the upstream lookup. MaskGuard filters the result. The worker writes back what survived the filter, heartbeats to keep the queue slot warm, and moves on. If the upstream returned masked, the slot is freed and the row gets tagged as a no-go work item — not a partial result, not an asterisk-row, a no-go row.
Why drip and not bulk: the privacy threat model is about partial state. A bulk scrape that pulls thousands of lookups and writes back some confirmed rows and some asterisk rows is a privacy event, regardless of whether the asterisks are then aggregated downstream for analytics. The claim/heartbeat pattern doesn't let that partial state exist. No-go until the upstream produces a full value. We will not optimize past that constraint to chase per-row throughput.
In practice, the runtime is on docket.decisionsciencecorp.com — API-first. The operator UI consumes the same wire that an external integration would consume. There is not going to be a "developer path" that loses the constraint and a "production path" that keeps it. One wire, one constraint, both audiences.
What's still honest
There are limits I won't pretend around. Bulk-import paths for existing customer lists are not yet wired through MaskGuard; they'll be in before any pilot goes live with a list longer than a few hundred rows. The operator audit log is internal-only today; it'll become a product-grade surface in a follow-on sprint. The MaskGuard ruleset is currently tuned for the upstream providers we ship against — if a future provider introduces a masking convention we haven't seen, the rule will write through cleanly the way it did with bv_unmasked.
We're not pretending the product is finished. We're saying the build constraint ships. A masked record does not pass the wire today; a partial record does not pass the wire today; a leaky debug table does not pass the wire today. The constraint is what the constraint looks like when it shows up at runtime, and the runtime is what we shipped.
If the next sprint is the one where the operator can show their general counsel an audit log of every enrichment event and which wire rules fired, that's the sprint that takes this MVP from "shipping" to "shippable for the operator's actual risk committee." We're aiming for that sprint.