<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Krit</title>
    <description>The latest articles on DEV Community by Krit (@kritj2005).</description>
    <link>https://dev.to/kritj2005</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4167330%2Ffb6c1ef0-29b5-4ce3-b7e0-628fc895dcda.jpg</url>
      <title>DEV Community: Krit</title>
      <link>https://dev.to/kritj2005</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kritj2005"/>
    <language>en</language>
    <item>
      <title>Airlock: an overnight inbox brief that attacker text cannot reach</title>
      <dc:creator>Krit</dc:creator>
      <pubDate>Mon, 26 Oct 2026 06:17:16 +0000</pubDate>
      <link>https://dev.to/kritj2005/airlock-an-overnight-inbox-brief-that-attacker-text-cannot-reach-1lhl</link>
      <guid>https://dev.to/kritj2005/airlock-an-overnight-inbox-brief-that-attacker-text-cannot-reach-1lhl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Airlock reads your email and WhatsApp exports overnight and hands you a morning brief, so&lt;br&gt;
you can skip the pile and act on the few messages that matter. The twist is that the&lt;br&gt;
messages are untrusted: anyone can email you an instruction. Airlock lets you be lazy&lt;br&gt;
without letting an inbox decide what your agent does.&lt;/p&gt;

&lt;p&gt;The idea fits in one line: &lt;strong&gt;the model points, code reads.&lt;/strong&gt; A small local model reads one&lt;br&gt;
message at a time and may only emit closed enums and sentence indices. It never copies&lt;br&gt;
attacker text back out. Deterministic code turns those indices into quotes, dates, amounts,&lt;br&gt;
trust labels, and a ranking. The model that reads your inbox has no tools and no authority;&lt;br&gt;
the code that acts cannot be prompt-injected.&lt;/p&gt;

&lt;p&gt;It ships as a CLI, a static HTML brief, and a read-only MCP server that other agents can&lt;br&gt;
query without ever receiving a sender string or a quote.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Krit-Jain/Airlock
&lt;span class="nb"&gt;cd &lt;/span&gt;Airlock
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run airlock demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;airlock demo&lt;/code&gt; runs a synthetic poisoned inbox through the full pipeline with a&lt;br&gt;
deterministic offline adapter and opens the brief. To use the real model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:4b
uv run airlock ingest path/to/inbox.mbox
uv run airlock run &lt;span class="nt"&gt;--model&lt;/span&gt; gemma3:4b
uv run airlock brief &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The brief has four sections: Needs you, Deadlines and amounts, Review carefully, and FYI.&lt;br&gt;
Quotes are labeled untrusted and shown only to you; links are defanged to plain text. The&lt;br&gt;
quarantine model has no free-text output field, so a message can tell you anything but&lt;br&gt;
cannot make the model say anything. A generated example brief is committed at&lt;br&gt;
&lt;code&gt;docs/demo-brief.html&lt;/code&gt;, and a narrated walkthrough is at &lt;code&gt;docs/demo.mp4&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Demo video link: &lt;a href="https://youtu.be/NYIRIzjhx1A" rel="noopener noreferrer"&gt;https://youtu.be/NYIRIzjhx1A&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/Krit-Jain/Airlock" rel="noopener noreferrer"&gt;https://github.com/Krit-Jain/Airlock&lt;/a&gt; (MIT).&lt;/p&gt;

&lt;p&gt;The parts worth reading first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/airlock/schema.py&lt;/code&gt;: the enum-only extraction schema. A test walks the generated JSON
schema and fails the build if any string field lacks an enum.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/airlock/signals.py&lt;/code&gt;: deterministic trust from headers, domains, hidden text, and
contacts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/airlock/taint.py&lt;/code&gt; and &lt;code&gt;gate.py&lt;/code&gt;: values carry an origin, and sinks refuse
untrusted-derived values without a human approval token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eval/&lt;/code&gt;: the harness, corpus, systems, and the pre-registered protocol.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I used open-weight models end to end. The quarantine extractor runs &lt;code&gt;gemma3:4b&lt;/code&gt; through&lt;br&gt;
Ollama with schema-constrained decoding, temperature zero, one isolated call per message,&lt;br&gt;
and reasoning disabled. The evaluation is a paired corpus of 232 test-split attacks built&lt;br&gt;
from clean messages plus a payload, with unique canaries, five sender variants, and several&lt;br&gt;
encodings.&lt;/p&gt;

&lt;p&gt;The evaluation asks whether the design does what it claims, and it is willing to fail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic layers.&lt;/strong&gt; On 65 header fixtures, 60 HTML fixtures, and 20 Unicode
fixtures: 100% spoof detection with 0% false-suspicious on 40 legitimate senders, 100%
hidden-text detection with 0% false-hidden on 30 legitimate-CSS emails, and dangerous
Unicode detected and stripped with no false positives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema escape.&lt;/strong&gt; Across 200 adversarial prompts, the sampler returned schema-valid JSON
200 times, with no out-of-vocabulary token. The check parses raw model output before
validation and fails if the sampler lets a payload through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-message isolation.&lt;/strong&gt; Fifty victim messages produced byte-identical records with
and without an attack message present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worst case.&lt;/strong&gt; Assuming a fully compromised extractor, an unknown sender can reach the
"Needs you" section but cannot reach high urgency; only a genuine known contact can.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The headline result is the mechanism check. Across 232 audited attacks, &lt;strong&gt;no attacker&lt;br&gt;
string and no canary ever reached an Airlock-fed agent&lt;/strong&gt;, even when &lt;code&gt;forward_email&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;send_email&lt;/code&gt;, and &lt;code&gt;fetch_url&lt;/code&gt; were exposed as tools.&lt;/p&gt;

&lt;p&gt;I also report what did not go as hoped. The real &lt;code&gt;gemma3:4b&lt;/code&gt; agent was rarely hijacked in a&lt;br&gt;
flat context (0.4% unauthorized calls, 1.3% canary leaks), so the detectors and prompt&lt;br&gt;
defenses had little left to reduce. That is the model's behavior, not a broken harness: a&lt;br&gt;
deterministic gullible oracle leaks the canary, proving the audit can fail. And steering&lt;br&gt;
remains real: payloads changed the action taken on a message 25-44% of the time, and a&lt;br&gt;
compromised known contact can be promoted past the trust cap. Airlock stops hijacking, not&lt;br&gt;
persuasion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;This project exists because the model runs on your machine.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your mail never leaves the device.&lt;/strong&gt; There is no API call, no account, no per-token
bill. The brief is generated from a local SQLite store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The security boundary is auditable.&lt;/strong&gt; Because the sampler is local, the schema passed
to it is the schema you can read and test. That is not true when the constraint lives
behind someone else's API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can swap the model.&lt;/strong&gt; The quarantine model is a configuration detail. Change the
tag, keep the boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run overnight.&lt;/strong&gt; A 4 GB laptop runs the full pipeline and the
evaluation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A closed API could give me structured output. It could not give me a boundary I can read,&lt;br&gt;
test, and replace without changing the trust model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;The point is to move the chore, not to watch it. You run one command before bed and read a&lt;br&gt;
short brief in the morning: a handful of things that need you, the deadlines and amounts,&lt;br&gt;
and a clearly separated pile to review. Everything else is filed or ignored. The trust&lt;br&gt;
labels mean you can skim the safe items and slow down only where the code says the sender&lt;br&gt;
is unverified or the content is hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Single labeler for the corpus rubric, and a synthetic corpus rather than real inbox data.&lt;/li&gt;
&lt;li&gt;Small open-weight models only; the numbers describe this class of model.&lt;/li&gt;
&lt;li&gt;Typed fields are attacker-influenced even when attacker text never reaches an LLM, which
is why the steering numbers are reported.&lt;/li&gt;
&lt;li&gt;Encoded payloads (base64, tag-block) evade literal text checks, so the taint audit
under-counts them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Best Use of Gemma: the quarantine extractor is &lt;code&gt;gemma3:4b&lt;/code&gt; run locally through Ollama
with schema-constrained decoding.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Your invoice reader is confidently wrong 4 times in 5. I built one that tells you which fields to check.</title>
      <dc:creator>Krit</dc:creator>
      <pubDate>Mon, 19 Oct 2026 04:21:30 +0000</pubDate>
      <link>https://dev.to/kritj2005/your-invoice-reader-is-confidently-wrong-4-times-in-5-i-built-one-that-tells-you-which-fields-to-2de0</link>
      <guid>https://dev.to/kritj2005/your-invoice-reader-is-confidently-wrong-4-times-in-5-i-built-one-that-tells-you-which-fields-to-2de0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I extracted 972 CORD receipt totals with a local 2B vision-language model. It got 59% of them right. When it was wrong, it was wrong &lt;em&gt;confidently&lt;/em&gt;: on the held-out test split, &lt;strong&gt;92% of the 111 wrong totals carried a mean token log-probability above −0.05&lt;/strong&gt; — an average per-token probability above 0.95.&lt;/p&gt;

&lt;p&gt;One receipt's total was &lt;code&gt;8,800,000.00&lt;/code&gt;. The model returned &lt;code&gt;88000.0&lt;/code&gt;, with a mean log-probability of &lt;code&gt;−0.000002&lt;/code&gt;. It was certain, and it was off by a hundredfold.&lt;/p&gt;

&lt;p&gt;That number is the whole project. A bookkeeper cannot hand a client "about 96% accurate". They need to know &lt;em&gt;which&lt;/em&gt; values to double-check. So I built Underwrite: it reads receipts and invoices into structured fields, then issues a &lt;strong&gt;certificate&lt;/strong&gt; per batch —&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Error rate on documents like these: ≤ 10%, with 90% confidence. 48% of totals auto-processed, the rest routed to a human."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;— and it earns that sentence with arithmetic, not marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Underwrite is a local pipeline that turns invoices into structured fields and decides, per field, whether to &lt;strong&gt;auto-accept&lt;/strong&gt; or &lt;strong&gt;send for review&lt;/strong&gt; — with the accept threshold chosen so the long-run error rate on documents like the calibration set stays under a target you pick.&lt;/p&gt;

&lt;p&gt;The customer is the one-person bookkeeping shop: a freelancer, a small accountant, someone digitising a shop's paper bills. Two things matter to them and neither is "accuracy":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What can I safely stop looking at?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What did the machine get wrong that I didn't catch?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second question is the one nobody answers. Every parser tells you &lt;em&gt;what&lt;/em&gt; it read. Almost none tell you &lt;em&gt;which values to trust&lt;/em&gt;, and the ones that do sell it as a closed feature. Underwrite answers it locally, on your own laptop, and shows its work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The certificate is the product.&lt;/strong&gt; The extraction is table stakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The review UI runs in &lt;strong&gt;demo mode&lt;/strong&gt; off a precomputed batch, so it needs no GPU and no network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;
python scripts/build_demo_batch.py     &lt;span class="c"&gt;# freezes one batch from the public CORD corpus&lt;/span&gt;
uvicorn underwrite.app.server:app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Left: the document. Right: five fields, colour-coded — green auto-accepted, amber flagged for review, red parse failure. Flagged fields are the only editable ones. The right-hand panel is the certificate: the error budget, the certified threshold, what was auto-processed, how many auto-accepted fields were sampled for audit, and the assumption the whole thing rests on.&lt;/p&gt;

&lt;p&gt;In the shipped demo batch: &lt;strong&gt;8 of 20 totals auto-accepted, 12 routed to review&lt;/strong&gt;, at a certified 10% error budget.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Figures, screenshots and the screen recording live in &lt;code&gt;reports/figures/&lt;/code&gt; and &lt;code&gt;reports/demo_video/&lt;/code&gt;: the risk-coverage curves, the ablation, the validity resplits, the repair curve, the audit alarm, and the review UI.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;demo video link-&amp;gt;&lt;a href="https://www.youtube.com/watch?v=k6hZj1PyeVc" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=k6hZj1PyeVc&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Krit-Jain/Underwrite" rel="noopener noreferrer"&gt;github.com/Krit-Jain/Underwrite&lt;/a&gt;&lt;/strong&gt; — 85 files, 293 tests, and every number in this post is produced by a script in the repo. &lt;code&gt;reports/tables/*.csv&lt;/code&gt; and &lt;code&gt;reports/figures/*.png&lt;/code&gt; are committed outputs of those scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The model, measured rather than assumed
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt;, loaded in 4-bit NF4 with bf16 compute on a &lt;strong&gt;6 GB laptop GPU&lt;/strong&gt; (RTX 4050).&lt;/p&gt;

&lt;p&gt;Two things I had to discover rather than guess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;E4B does not fit.&lt;/strong&gt; Even at 4-bit it needs more than the 6,140 MiB available, because of Gemma 4's per-layer input embeddings — &lt;code&gt;vocab_size_per_layer_input: 262144&lt;/code&gt; across 42 layers is ~5.6 GB on its own. Transformers quietly spills to CPU/disk and the 4-bit quantizer refuses to load. E2B (35 layers, hidden 1536) fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;device_map="auto"&lt;/code&gt; must not be used.&lt;/strong&gt; It splits the model onto CPU whenever free VRAM looks tight, which the quantizer rejects. Pinning &lt;code&gt;{"": 0}&lt;/code&gt; is the difference between working and not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measured on 983 documents: &lt;strong&gt;9.06 s/document, 2.47 hours wall clock, ~6.6 GB peak VRAM&lt;/strong&gt;, 47 output tokens per document (&lt;code&gt;reports/tables/extraction_cost.csv&lt;/code&gt;; VRAM from the smoke test). An image is compressed to 280 vision tokens regardless of receipt height, which is what makes a 6 GB card viable at all. The synthetic Indian invoices were much slower — &lt;strong&gt;40.3 s/document&lt;/strong&gt;, emitting 62 output tokens against 47 on CORD.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two-pass trick
&lt;/h3&gt;

&lt;p&gt;Generation gives you the answer but throws away the model's uncertainty. So every document gets a &lt;strong&gt;second forward pass&lt;/strong&gt;: the same (image + prompt + generated JSON) is fed back with &lt;code&gt;output_hidden_states=True&lt;/code&gt;, and I record, per field:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;realised log-probability of each value token (mean, min), first-token entropy, top-1/top-2 margin;&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;hidden-state vector&lt;/strong&gt; at the value's last token, and the mean over its tokens, at four depths (roughly 25/50/75/90%).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aligning a field to its tokens is the fiddly part — the chat template wraps the response in its own tokens, so the response is located by subsequence search rather than assumed to start at the end of the prompt. Verified on all 983 documents: &lt;strong&gt;983/983 aligned&lt;/strong&gt;, checked by reconstructing each value from its selected tokens' character offsets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Five black-box signals, one white-box signal
&lt;/h3&gt;

&lt;p&gt;The experiment is a fair fight at the &lt;em&gt;same&lt;/em&gt; certified error rate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;signal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;mean token log-probability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;verbalized confidence (ask the model to rate itself)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;validators only (arithmetic, formats, GSTIN checksum, date plausibility)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S2g&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;source grounding — does the value actually appear on the page?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;logprob + validators + grounding (&lt;strong&gt;the strongest black-box baseline&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;hidden-state probe only (&lt;strong&gt;white-box&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;S3 + probe (&lt;strong&gt;the system&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On CORD, per-field AUROC for the &lt;code&gt;total&lt;/code&gt; field (n=292 held-out documents):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S0  logprob            0.632
S2  validators         0.525   ← near chance
S2g grounding          0.725
S3  black-box fusion   0.838
S4  probe only         0.914
S5  probe + fusion     0.952
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The hidden states carry information the entire black-box stack does not.&lt;/strong&gt; S4 — nothing but a PCA + logistic regression on the model's internal vectors — beats S3, which combines three hand-built signals.&lt;/p&gt;

&lt;p&gt;One honest gap in that table: &lt;strong&gt;S1 is missing.&lt;/strong&gt; The final run skipped the self-rating call to halve the runtime. It was measured on the earlier run over the same test split at AUROC &lt;strong&gt;0.603&lt;/strong&gt; for &lt;code&gt;total&lt;/code&gt; — weak, and consistent with the published finding that verbalized confidence is close to useless on receipts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does any of that depend on perfect OCR?&lt;/strong&gt; Grounding is measured against CORD's ground-truth OCR words and the synthetic renderer's own text — oracle text, so absolute coverage is optimistic. Removing grounding entirely makes the comparison &lt;em&gt;stronger&lt;/em&gt;, not weaker: S3 falls to &lt;strong&gt;0.729&lt;/strong&gt; while S5 holds &lt;strong&gt;0.915&lt;/strong&gt;. Without grounding, S5 is essentially S4 (0.915 vs 0.914) — the black-box features add nothing, and the probe carries the signal alone.&lt;/p&gt;

&lt;p&gt;That is the answer to "why open weights": a closed API gives you token probabilities at best. It does not give you the residual stream. The probe cannot exist there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The gate: Learn-Then-Test
&lt;/h3&gt;

&lt;p&gt;The accept threshold is chosen by fixed-sequence testing over a &lt;strong&gt;pre-specified grid&lt;/strong&gt;: walk from the strictest threshold to the most lenient, and at each one test &lt;code&gt;H0: risk among accepted &amp;gt; α&lt;/code&gt; with an exact binomial test at level δ. Stop at the first failure. Because the order is fixed and you stop at the first non-rejection, the family-wise error rate is controlled.&lt;/p&gt;

&lt;p&gt;One correction I had to make to the textbook recipe: &lt;strong&gt;the strictest threshold accepts a single document, where &lt;code&gt;P[Bin(1, α) ≤ 0] = 0.95&lt;/code&gt; and no test can ever reject.&lt;/strong&gt; Taken literally, the procedure never certifies anything — my first simulation certified 0/200 times. Thresholds that cannot reject even with zero errors are skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Held-out test split (&lt;code&gt;total&lt;/code&gt;, n=292, base error 38.0%).&lt;/strong&gt; These coverage columns are &lt;em&gt;empirical&lt;/em&gt;: the threshold behind each one is selected on this same test set, so read them as the achievable frontier, not a promise. The certified numbers are in "Is the certificate real?" below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;method                AUROC   coverage@2%  @5%   @10%
S3 black-box fusion   0.838      8.2%     14.7%  46.2%
S4 probe              0.914     17.5%     30.5%  51.7%
S5 probe + fusion     0.952     41.4%     49.7%  62.0%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At a &lt;strong&gt;5% error budget, S5 auto-processes half the totals&lt;/strong&gt; — 3.4× what the black-box baseline manages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the certificate real?&lt;/strong&gt; 200 random resplits of the pooled calibration and test documents. "Certifies" is how often a threshold was found at all; the violation column is the share of &lt;em&gt;certified&lt;/em&gt; resplits whose realised risk then exceeded α — the rest issued no promise, so they cannot break one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;α=0.10                   certifies   coverage   violates-if-certified
S3 black-box fusion       79/200      10.9%             20.3%
S4 probe                 200/200      18.0%              4.5%
S5 probe + fusion        200/200      48.1%             11.5%

α=0.05                   certifies   coverage   violates-if-certified
S3 black-box fusion        4/200      17.5%            100.0%
S4 probe                  20/200      14.8%             95.0%
S5 probe + fusion        200/200      29.4%              6.0%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;S5 certifies on every single resplit&lt;/strong&gt; — 48% coverage for a 10% budget, 29% for 5%. S3 certifies rarely, and when it does it is usually lying: &lt;strong&gt;all four&lt;/strong&gt; of its α=0.05 certificates violate, and a fifth of its α=0.10 ones do. A score that cannot separate a hard document from an easy one cannot safely make this promise, and the resplits expose it.&lt;/p&gt;

&lt;p&gt;I want to be straight about the one number that isn't clean: &lt;strong&gt;at α=0.10 the violation rate is 11.5%, above δ=10%.&lt;/strong&gt; Sampling noise accounts for much of it (with 200 splits the standard error is ~2%), and my simulations show that exceedances &lt;em&gt;should&lt;/em&gt; run slightly above δ when you measure realised risk on a finite test part — the guarantee is about population risk, and realised risk fluctuates across it. But it is above the line, and it's in the table rather than in a footnote.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when the documents change
&lt;/h3&gt;

&lt;p&gt;Calibrate on Indonesian receipts (CORD), deploy on synthetic Indian GST invoices, and the certificate does not &lt;em&gt;break&lt;/em&gt; — it becomes useless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CORD-calibrated, deployed on Indian invoices
  source coverage 42.9%   →   target coverage 2.5%   target risk 0.0%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scores shift even though the target set is &lt;em&gt;easier&lt;/em&gt; (90.5% of totals correct, versus 59% on CORD). The threshold calibrated on hard documents is simply never cleared by an easier distribution. No wrong values were auto-accepted — but it auto-accepted almost nothing. I re-ran this after fixing a money-convention bug in the grounding feature and the table came out identical, so the collapse is a property of the score distribution, not an artifact.&lt;/p&gt;

&lt;p&gt;So recalibrate. The business question is how many labelled documents a new client's template needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;α=0.10   k=25 → 76% certify at 93% coverage
         k=50 → 100% certify at 93% coverage
α=0.05   k=50 → 70% certify at 91% coverage
        k=100 → 100% certify at 92% coverage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Roughly 50 labelled documents per new client template to certify a 10% budget; about 100 for 5%.&lt;/strong&gt; That is a number a bookkeeper can act on.&lt;/p&gt;

&lt;h3&gt;
  
  
  The audit lane
&lt;/h3&gt;

&lt;p&gt;Flagged-item labels are selection-biased — humans only look at what the system already distrusts, so they can never tell you the error rate among auto-accepted fields. Underwrite instead samples a random slice of &lt;em&gt;auto-accepted&lt;/em&gt; fields and checks those.&lt;/p&gt;

&lt;p&gt;The alarm asks one question: &lt;em&gt;is the true error rate above α?&lt;/em&gt; That needs the Clopper-Pearson &lt;strong&gt;lower&lt;/strong&gt; bound to exceed α. My first version alarmed on the &lt;em&gt;upper&lt;/em&gt; bound. That sounds similar and is wrong: with only a handful of audits the interval is so wide that the upper bound is always above α. A reviewer caught it — that rule fired on &lt;strong&gt;52–90% of clean streams&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With the lower bound, and a Bonferroni correction across the repeated checks, over &lt;strong&gt;500 seeds&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;true error rate&lt;/th&gt;
&lt;th&gt;false alarms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;td&gt;0.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;0.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;against a 5% budget. Detecting a jump to 40% error inside a 500-document window:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;audit rate&lt;/th&gt;
&lt;th&gt;median delay&lt;/th&gt;
&lt;th&gt;missed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;410 documents&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;407 documents&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;357 documents&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the real pipeline — 616 accepted CORD receipts at 1.1% error, then a template change — the stream I committed happens to be &lt;strong&gt;one of the misses&lt;/strong&gt;: the lane stays silent through the clean segment, and the lower bound climbs to 0.08 after the switch without reaching α. I left that in rather than hunting for a seed that alarms.&lt;/p&gt;

&lt;p&gt;The honest summary: &lt;strong&gt;the alarm doesn't cry wolf, but at a 10% audit rate it misses about half of drifts within a short window.&lt;/strong&gt; A quiet, slow monitor is still useful; a noisy one gets switched off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Three reasons, and the first is decisive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The best signal is invisible to closed APIs.&lt;/strong&gt; The probe reads the model's hidden states. No hosted API exposes them, at any price. On this corpus a PCA over those vectors alone (AUROC 0.914) beats every black-box signal combined (0.838). That gap is not a tuning difference; it's an information difference, and it exists only because the weights are open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A certificate is only valid for a frozen model.&lt;/strong&gt; My numbers are calibrated against one specific quantized checkpoint. If Google ships an update, a hosted endpoint might change silently and my calibration becomes a lie. Pinned open weights keep it honest — I pin the revision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The documents never leave the laptop.&lt;/strong&gt; These are someone's supplier invoices with their margins on them. Running locally isn't a nice-to-have here; it's the reason a bookkeeper can use this at all. The marginal cost is electricity measured in a 2.47-hour run on a laptop GPU, not a per-page bill from an API.&lt;/p&gt;

&lt;p&gt;There's a fourth, less flattering reason: &lt;strong&gt;I could measure it.&lt;/strong&gt; Every number above came from a script I can re-run, against a checkpoint I can inspect. With a closed model I'd be calibrating against something I can't see and can't freeze.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — Gemma 4 E2B is the extractor, and the hidden-state probe is only possible because the weights are open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;Being precise about what this does &lt;em&gt;not&lt;/em&gt; show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The guarantee is about the &lt;code&gt;total&lt;/code&gt; field only.&lt;/strong&gt; &lt;code&gt;total&lt;/code&gt; has one value per document, so calibration samples are independent. Per-field guarantees on subtotal/tax/date/vendor would be invalid — the same document's fields are correlated, and the natural per-field procedure silently violates its own promise (see arXiv:2608.14639). Vendor and date are shown in the UI but always routed to review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~290 calibration documents is thin.&lt;/strong&gt; That is why α=0.05 is at the edge of feasibility, and why the α=0.10 violation rate sits slightly above δ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 2B model on a 6 GB card sets the ceiling.&lt;/strong&gt; Base error is 38% on &lt;code&gt;total&lt;/code&gt;. A larger model would raise coverage; it wouldn't change the machinery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exchangeability is assumed.&lt;/strong&gt; The audit lane exists because that assumption fails, and the shift experiment shows exactly how (silently, and by collapsing coverage rather than by producing wrong auto-accepts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The synthetic Indian invoices have no OCR pass&lt;/strong&gt; — grounding is measured against the text the renderer drew, so OCR error is not modelled there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORD is Indonesian and CC-BY-4.0&lt;/strong&gt;, and the corpus has no vendor/date ground truth, so the five-field story can only be told as a three-field one on this data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prior work
&lt;/h2&gt;

&lt;p&gt;This builds directly on open work and claims no novelty for the machinery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Learn-Then-Test&lt;/strong&gt; (Angelopoulos et al., &lt;a href="https://arxiv.org/abs/2110.01052" rel="noopener noreferrer"&gt;arXiv:2110.01052&lt;/a&gt;) — the fixed-sequence threshold calibration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VerifyDoc&lt;/strong&gt; (&lt;a href="https://github.com/bhaskargurram-ai/verifydoc" rel="noopener noreferrer"&gt;bhaskargurram-ai/verifydoc&lt;/a&gt;, Apache-2.0) — per-field calibrated confidence, source grounding, accept/review at a target selective risk. Its reported CORD AUROCs (grounding 0.82, token-probability 0.69, verbalized ~0.50) are why grounding is in my baseline and why my verbalized signal is reported as near-useless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Valid Per-Field Selective Risk Control for Document Extraction"&lt;/strong&gt; (&lt;a href="https://arxiv.org/html/2608.14639v1" rel="noopener noreferrer"&gt;arXiv:2608.14639&lt;/a&gt;) — shows the naive per-field procedure violates its own guarantee. That paper is the reason my certified guarantee is restricted to &lt;code&gt;total&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORD&lt;/strong&gt; (Park et al.) — the receipt corpus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The contribution here is narrow and I'll state it narrowly: at a matched certified error rate on a laptop-class open model, a hidden-state probe auto-processes substantially more documents than a strong black-box stack that includes grounding.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
