<?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: Zhuoli Liang</title>
    <description>The latest articles on DEV Community by Zhuoli Liang (@zhuoli_liang_b6b05a8c3c1c).</description>
    <link>https://dev.to/zhuoli_liang_b6b05a8c3c1c</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%2F2744561%2Fe90d32f6-cc45-44b1-b42f-36a0bb28cf59.jpg</url>
      <title>DEV Community: Zhuoli Liang</title>
      <link>https://dev.to/zhuoli_liang_b6b05a8c3c1c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zhuoli_liang_b6b05a8c3c1c"/>
    <language>en</language>
    <item>
      <title>Which clause wins? I benchmarked whether LLMs follow the rules that actually govern</title>
      <dc:creator>Zhuoli Liang</dc:creator>
      <pubDate>Wed, 07 Oct 2026 16:04:27 +0000</pubDate>
      <link>https://dev.to/zhuoli_liang_b6b05a8c3c1c/which-clause-wins-i-benchmarked-whether-llms-follow-the-rules-that-actually-govern-4ogn</link>
      <guid>https://dev.to/zhuoli_liang_b6b05a8c3c1c/which-clause-wins-i-benchmarked-whether-llms-follow-the-rules-that-actually-govern-4ogn</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I benchmarked, and why
&lt;/h2&gt;

&lt;p&gt;I enter a lot of contests and bounties. The rules for a single contest are never on one page. There's a landing page, an FAQ, a contest-rules page, and the platform's general Official Rules, and they drift apart. While building &lt;strong&gt;Fine Print&lt;/strong&gt; (an agent that reads contest rules for you), I kept hitting the same question: when the friendly FAQ says one thing and the Official Rules say another, &lt;em&gt;does the model notice which one controls?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Fine Print Bench&lt;/strong&gt;: fictional rule packets, each with a plain question ("What is the controlling close instant in UTC?", "Is a team of five allowed?", "Which required artifacts are missing?"). The model has to return a typed answer, the &lt;strong&gt;minimal clause IDs&lt;/strong&gt; that justify it, and a confidence. 160 items in two splits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;easy&lt;/strong&gt; (80 items): short packets, one precedence hop, eight categories:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What it isolates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Explicit authority&lt;/td&gt;
&lt;td&gt;Rules vs. FAQ/landing/blog when a precedence sentence exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope &amp;amp; amendments&lt;/td&gt;
&lt;td&gt;A &lt;em&gt;newer&lt;/em&gt; FAQ is not an &lt;em&gt;Amendment&lt;/em&gt;; an amendment that only names Track B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unresolved authority&lt;/td&gt;
&lt;td&gt;Equal-authority pages with no tie-break, plus controls where the pages agree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact deadlines&lt;/td&gt;
&lt;td&gt;DST ending the same day, the date line, a submission 30 seconds late&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business-day windows&lt;/td&gt;
&lt;td&gt;Holidays, "counting the decision date as day one", a trigger date nobody gave you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eligibility&lt;/td&gt;
&lt;td&gt;Missing residency, a subsidiary employer, age of majority â‰&amp;nbsp; 18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Submission artifacts&lt;/td&gt;
&lt;td&gt;Conditional requirements, a draft post, a private repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notification &amp;amp; payout&lt;/td&gt;
&lt;td&gt;"first &lt;em&gt;attempted&lt;/em&gt; notification" vs. "after you receive our email"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;hard&lt;/strong&gt; (80 items): real-length rules (~800 words of boilerplate around the deciding clause), two-hop precedence (Amendment â†’ Contest Page â†’ Rules, withdrawn amendments, circular deference), IANA zone names around DST gaps/overlaps and the date line, business-day chains in a named zone, multi-turn corrections that flip the answer, compound packets, and aggregation over 6â€“40 entrant records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About a fifth of the packets have no unique answer, and the correct output is &lt;code&gt;insufficient_information&lt;/code&gt;. "Always abstain" can't win, because determinate and unresolved accuracy are reported separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The twist: every packet appears twice.&lt;/strong&gt; The second copy is semantically identical. Only the source order, the fact order or the record order changes, or an irrelevant "About the organizer" page is added. A model that reads rules carefully should give the same answer both times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoring (no LLM judge)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict accuracy:&lt;/strong&gt; the status and value are correct (timestamps compared as instants; local-time questions must also use the requested offset), &lt;strong&gt;and&lt;/strong&gt; the cited clauses equal a minimal evidence set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer-only accuracy:&lt;/strong&gt; the value alone, so citation strictness and reasoning errors can be told apart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation flip rate:&lt;/strong&gt; the share of packet pairs where the &lt;em&gt;answer&lt;/em&gt; (status + canonical value) changed after a harmless reorder. Set order, whitespace, offset notation and the reason label don't count as flips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confident-wrong rate:&lt;/strong&gt; the share of answers with self-reported confidence â‰¥ 0.9 that were wrong.&lt;/li&gt;
&lt;li&gt;95% CIs come from a bootstrap over packet families (40 per split). Both copies of a packet are resampled together, so 80 correlated items aren't treated as 80 independent ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every calendar answer is recomputed by a second, independent oracle (Python &lt;code&gt;zoneinfo&lt;/code&gt;); every aggregation answer by a second implementation that parses the clause text itself. There are corruption tests for the scorer itself. Every model gets the same plain-text prompt and the same tolerant parser (reasoning blocks stripped, JSON pulled from code fences); rate limits and outages are retried and reported as &lt;em&gt;coverage&lt;/em&gt;, never scored as wrong answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models
&lt;/h2&gt;

&lt;p&gt;Kaggle's default benchmark model (&lt;code&gt;google/gemini-3.7-flash&lt;/code&gt;) for the leaderboard task, then the same 160 items on &lt;code&gt;google/gemini-2.5-flash&lt;/code&gt;, &lt;code&gt;google/gemma-4-31b&lt;/code&gt;, &lt;code&gt;qwen/qwen3-235b-a22b-instruct-2507&lt;/code&gt;, &lt;code&gt;deepseek-ai/deepseek-r1-0528&lt;/code&gt; and &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt;: two generations of the same provider (does a newer model fix authority errors?), an open-weights model from the same family, and three other open-weights families including two reasoning models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Final results (run 4, 2026-10-07 PT).&lt;/strong&gt; The default model's row is the &lt;strong&gt;leaderboard run&lt;/strong&gt; (Kaggle run 3, notebook Version 1); gemini-2.5-flash is from run 2 (same items except one clarified packet, see below). Qwen3-235B, DeepSeek-R1 and Gemma-4-31B are from the run 4 exploration (&lt;code&gt;results/run4/explore&lt;/code&gt;). Qwen and DeepSeek answered all 160 items. &lt;strong&gt;Gemma has 150 of 160 results: 10 hard items are missing because Kaggle's model proxy rate-limited them&lt;/strong&gt;, so its hard score is over 70 items, not 80. &lt;strong&gt;gpt-oss-120b has no results at all:&lt;/strong&gt; Kaggle's model proxy kept returning HTTP 429 (rate limit) on every attempt, so I report nothing for it rather than counting the failures as wrong answers. Numbers are strict accuracy unless noted; flip rate counts packet pairs; confident-wrong = wrong among answers with confidence â‰¥ 0.9. 95% CIs (brackets) were computed for the two Gemini rows only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Easy strict&lt;/th&gt;
&lt;th&gt;Hard strict&lt;/th&gt;
&lt;th&gt;Hard answer-only&lt;/th&gt;
&lt;th&gt;Hard flip rate&lt;/th&gt;
&lt;th&gt;Hard confident-wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gemini-3.7-flash (default, leaderboard run)&lt;/td&gt;
&lt;td&gt;0.975 [0.938, 1.0]&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.000&lt;/td&gt;
&lt;td&gt;0.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemini-2.5-flash&lt;/td&gt;
&lt;td&gt;0.975 [0.938, 1.0]&lt;/td&gt;
&lt;td&gt;0.850 [0.775, 0.925]&lt;/td&gt;
&lt;td&gt;0.850&lt;/td&gt;
&lt;td&gt;0.275&lt;/td&gt;
&lt;td&gt;0.150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma-4-31b (150/160; 70 hard)&lt;/td&gt;
&lt;td&gt;0.988&lt;/td&gt;
&lt;td&gt;0.929&lt;/td&gt;
&lt;td&gt;0.929&lt;/td&gt;
&lt;td&gt;0.057 (2/35)&lt;/td&gt;
&lt;td&gt;0.071&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3-235b-a22b-instruct-2507&lt;/td&gt;
&lt;td&gt;0.738&lt;/td&gt;
&lt;td&gt;0.275&lt;/td&gt;
&lt;td&gt;0.350&lt;/td&gt;
&lt;td&gt;0.400 (16/40)&lt;/td&gt;
&lt;td&gt;0.646&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-r1-0528&lt;/td&gt;
&lt;td&gt;0.900&lt;/td&gt;
&lt;td&gt;0.650&lt;/td&gt;
&lt;td&gt;0.688&lt;/td&gt;
&lt;td&gt;0.200 (8/40)&lt;/td&gt;
&lt;td&gt;0.312&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-oss-120b&lt;/td&gt;
&lt;td&gt;&lt;em&gt;no results: every call got HTTP 429 from Kaggle's model proxy&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notes on these numbers. The default model answered all 160 items correctly (answer-only 1.000, zero flips) in the leaderboard run; its two strict misses on easy were citations written as &lt;code&gt;rules:S1&lt;/code&gt; instead of &lt;code&gt;S1&lt;/code&gt;. The scorer now accepts a source-qualified ID when the prefix really is that clause's source, which makes it 160/160 â€” but the leaderboard number stays 0.975 + 1.000 as run. In run 2 it scored 0.988 / 1.000. For gemini-2.5-flash (run 2): one easy packet (&lt;code&gt;payout_02&lt;/code&gt;) now also accepts citing R2 alone, and one hard packet (&lt;code&gt;h_mix_03&lt;/code&gt;) had a judge-supplier clause that could be read as a competing permission, so I clarified its wording; on the 78 unchanged hard items it scores 0.872. The pending runs use the clarified keys.&lt;/p&gt;

&lt;p&gt;gemini-2.5-flash per hard category: aggregation_xl 0.6, tz_dst 0.7, business_day_chains 0.8, compound 0.7; long distractors, two-hop precedence, multi-turn and 6â€“12-record aggregation all 1.0. Its easy split is near ceiling (0.975).&lt;/p&gt;

&lt;p&gt;Overall strict accuracy (all items): Gemma-4-31B 0.960 (n=150), DeepSeek-R1-0528 0.775 (n=160), Qwen3-235B 0.506 (n=160).&lt;/p&gt;

&lt;p&gt;Per-category strict accuracy, both splits combined (run 4 exploration; &lt;code&gt;results/run4/fine_print_per_category.csv&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;DeepSeek-R1&lt;/th&gt;
&lt;th&gt;Gemma-4-31B&lt;/th&gt;
&lt;th&gt;Qwen3-235B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;explicit_authority&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scope_amendment&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unresolved_authority&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;two_hop_precedence (hard)&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;long_distractors (hard)&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;multi_turn (hard)&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exact_deadlines&lt;/td&gt;
&lt;td&gt;0.3&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tz_dst (hard)&lt;/td&gt;
&lt;td&gt;0.3&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;business_days&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;business_day_chains (hard)&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eligibility&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;submission_artifacts&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;notification_payout&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;compound (hard)&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;0.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aggregation (hard)&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aggregation_xl (hard)&lt;/td&gt;
&lt;td&gt;0.2&lt;/td&gt;
&lt;td&gt;n/a (rate-limited)&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Format errors:&lt;/strong&gt; zero for every model that returned answers. The shared parser recovered JSON from every Gemma, Qwen and DeepSeek response, so every miss above is a real wrong answer or a wrong citation, not a parsing failure. The only non-answers were gpt-oss-120b's HTTP 429s and Gemma's 10 rate-limited items, and those are counted as missing coverage, not as errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The 31B model beat the bigger ones.&lt;/strong&gt; Gemma-4-31B scored 0.988 easy / 0.929 hard strict, ahead of DeepSeek-R1 (a 671B reasoning model, 0.650 hard) and Qwen3-235B (0.275 hard). It never followed an FAQ over the Rules and never broke on long distractors or two-hop precedence. Its only real weak spot was DST edge cases (0.6), which also hurt every other model. The caveat: Gemma's hard score covers 70 of 80 items, because the 10 largest aggregation items were rate-limited. Even if it had missed all 10, it would still be ahead of DeepSeek on hard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Qwen is confidently wrong about &lt;em&gt;which clause governs&lt;/em&gt;.&lt;/strong&gt; Qwen3-235B is the only model that actually lost authority questions: 0.6 on two-hop precedence, 0.2 on long-distractor packets, 0.7 on explicit authority. On those misses it usually cited the wrong clause (for example, a Track page over the Amendment that replaced it) with confidence 0.95â€“1.0. On the hard split, 65% of its answers that claimed confidence â‰¥ 0.9 were wrong. Its self-reported confidence carries almost no information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Flip rate falls as accuracy rises.&lt;/strong&gt; On the hard split: Qwen 0.275 strict / 0.40 flips, DeepSeek-R1 0.650 / 0.20, Gemma 0.929 / 0.057, gemini-3.7-flash 1.000 / 0.000. (gemini-2.5-flash, 0.850 / 0.275, is the outlier: it flips more than its accuracy predicts.) When a model gets a hard item wrong, a harmless reorder often gives a &lt;em&gt;different&lt;/em&gt; wrong answer. In other words, instability tells you the model doesn't know. It is a cheap and useful signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Reasoning didn't fix the calendar.&lt;/strong&gt; DeepSeek-R1 writes long chains of thought, but it scored 0.3 on exact deadlines and DST. On several DST-gap packets it answered &lt;code&gt;insufficient_information&lt;/code&gt; with confidence â‰¥ 0.95, even though the packet said exactly what to do when the local time doesn't exist. It did fine on authority (1.0 on two-hop and long distractors), so its failures are arithmetic and abstention, not reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The older Gemini breaks on &lt;em&gt;presentation&lt;/em&gt;, not authority.&lt;/strong&gt; gemini-3.7-flash, Kaggle's default model, gave the same correct answer to every packet in both orders. gemini-2.5-flash got every precedence question right, but its hard-split losses move with harmless reorders: on 11 of 40 hard packet pairs its answer changed between the two copies.&lt;/p&gt;

&lt;p&gt;Three concrete cases (rule: the most common error type in each failing hard category, gemini-2.5-flash, run 2):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reordering a 40-record list changed the answer&lt;/strong&gt; (&lt;code&gt;aggregation_xl&lt;/code&gt;). Same clauses, same records, record order reversed: the base copy and the reversed copy disagreed on which IDs are disqualified in 4 of 5 packets; every one of those answers, right or wrong, had confidence â‰¥ 0.9.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A DST gap with an explicit rule became "insufficient information"&lt;/strong&gt; (&lt;code&gt;tz_dst&lt;/code&gt;). "Entries close at 2:30 AM on October 4, 2026, Australia/Sydney local time. If that local time does not exist because of a daylight-saving change, the deadline is the first valid local time after it." The base copy answered &lt;code&gt;insufficient_information&lt;/code&gt; (missing_fact, confidence 1.0); the variant, with an irrelevant page added, answered the correct 2026-10-03T16:00:00Z.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Off-by-one in a Tokyo business-day chain&lt;/strong&gt; (&lt;code&gt;business_day_chains&lt;/code&gt;). Paperwork due three business days after first attempted notification, dates in Asia/Tokyo, with a listed holiday: the base copy got the correct 2026-11-05; the variant, with the two clauses in reverse order, said 2026-11-04 (confidence 1.0).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From run 4, using the same rule:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3-235B: the wrong governing clause, with high confidence&lt;/strong&gt; (&lt;code&gt;two_hop_precedence&lt;/code&gt;). In &lt;code&gt;h_hop_04&lt;/code&gt; and &lt;code&gt;h_hop_05&lt;/code&gt;, an Amendment overrides a Track page, which overrides the Rules. Qwen cited the Rules plus the Track clause, skipped the Amendment, and answered at confidence 0.95â€“1.0. On &lt;code&gt;h_hop_05&lt;/code&gt; the two copies even disagreed (true vs. false).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeepSeek-R1: abstaining on a DST rule it was given&lt;/strong&gt; (&lt;code&gt;tz_dst&lt;/code&gt;). For &lt;code&gt;h_tz_01&lt;/code&gt;, &lt;code&gt;h_tz_03&lt;/code&gt; and &lt;code&gt;h_tz_05&lt;/code&gt;, it answered &lt;code&gt;insufficient_information&lt;/code&gt; at confidence 0.95â€“1.0, although the packet contained an explicit "if that local time does not exist" rule. Reasoning tokens didn't fix this. Both reasoning-heavy models I could run treated the tricky clock as missing data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma-4-31B: the same failure in a smaller dose&lt;/strong&gt; (&lt;code&gt;tz_dst&lt;/code&gt;). It abstained on the same DST-gap packet in both copies, and on a Nepal (+05:45) packet one copy abstained while the other committed to an answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Gemma and DeepSeek never followed an FAQ or landing page over the Rules when a precedence sentence was present. Qwen was the only model with results that did (explicit authority 0.7). gpt-oss-120b has no results (every call got HTTP 429), so I can't say whether it fixes the arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it changed about how I build agents
&lt;/h2&gt;

&lt;p&gt;I went in expecting authority to be the hard part, meaning whether the model knows the Rules beat the FAQ. For the good models, it isn't. Gemma and both Gemini models basically never chose the wrong source. What broke was arithmetic (DST, business days, counting records) and consistency (the same question in a different order got a different answer).&lt;/p&gt;

&lt;p&gt;So this is how I build Fine Print now, and how I'd build any agent that reads documents with legal weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model picks the clause. Code does the math.&lt;/strong&gt; The LLM's job is to say "R4 governs, and here is the instant it names". Deadlines, time zones, business-day windows and record filters go through deterministic code with &lt;code&gt;zoneinfo&lt;/code&gt; and a holiday list. Every model I tested was weakest exactly where a Python function would be perfect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it twice, shuffled.&lt;/strong&gt; Every extraction runs twice with the sources reordered. If the two answers disagree, it goes to a human. The flip-rate result is why: disagreement is cheap to detect, and it shows up most on the items the model actually gets wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust self-reported confidence.&lt;/strong&gt; Almost every answer from every model claimed â‰¥ 0.9. For Qwen, 65% of those were wrong on hard items. Confidence isn't the signal. Agreement and verifiable citations are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the model by measurement, not size.&lt;/strong&gt; A 31B open model beat a 235B and a 671B model on this task. I would never have guessed that, and I'd have paid for the bigger one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is new advice. But I wouldn't have known which parts to move into code until I measured where the models actually fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd measure next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The same packets with deterministic date tools available: does the tool fix the arithmetic and the order-sensitivity?&lt;/li&gt;
&lt;li&gt;More presentation variants per packet (k &amp;gt; 2), so flip rate has tighter intervals&lt;/li&gt;
&lt;li&gt;Real (licensed) rule pages instead of fictional ones&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to see it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark on Kaggle:&lt;/strong&gt; &lt;a href="https://www.kaggle.com/code/zhuoliliang/new-benchmark-task-e35ea" rel="noopener noreferrer"&gt;https://www.kaggle.com/code/zhuoliliang/new-benchmark-task-e35ea&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code, items and tests:&lt;/strong&gt; &lt;a href="https://github.com/Zhuoli/fine-print-bench" rel="noopener noreferrer"&gt;https://github.com/Zhuoli/fine-print-bench&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Built with AI coding agents (Codex and Grok). I designed the question and checked the gold answers. Two different agents brainstormed the categories and implemented the scorer, and a second oracle checks every calendar answer.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Out the Door: tell Gemma what you want to do outside, get one time, close the tab</title>
      <dc:creator>Zhuoli Liang</dc:creator>
      <pubDate>Mon, 05 Oct 2026 21:26:08 +0000</pubDate>
      <link>https://dev.to/zhuoli_liang_b6b05a8c3c1c/out-the-door-tell-gemma-what-you-want-to-do-outside-get-one-time-close-the-tab-51e9</link>
      <guid>https://dev.to/zhuoli_liang_b6b05a8c3c1c/out-the-door-tell-gemma-what-you-want-to-do-outside-get-one-time-close-the-tab-51e9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Out the Door&lt;/strong&gt; answers one question: &lt;em&gt;when should I go outside today?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I work for myself, at a desk, in Seattle. "I'll go for a walk later" is a very easy promise to break. And when I do mean it, I open a weather app and start reading 72 rows of hourly numbers. Is 40% rain at 3 PM worse than 15 km/h wind at 5? Does a 45-minute walk still fit before sunset? Ten minutes later I'm still on my phone, and it's getting dark.&lt;/p&gt;

&lt;p&gt;So the app is built to be the shortest part of going outside:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Say what you want in your own words: &lt;em&gt;"take the pup around the lake once I log off at 5:30, I don't want to get rained on"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma, running on your own computer&lt;/strong&gt;, turns that into a plan: activity, duration, time limits, which day, what to avoid.&lt;/li&gt;
&lt;li&gt;Plain code scores every hour of the free Open-Meteo forecast for that activity (rain chance and millimetres, wind and gusts, feels-like temperature, UV, daylight from sunrise and sunset) and picks the best window.&lt;/li&gt;
&lt;li&gt;You get &lt;strong&gt;one time&lt;/strong&gt;, what to bring, and a 3-sentence "door note" from Gemma. Then three buttons: &lt;strong&gt;add the reminder&lt;/strong&gt; (an &lt;code&gt;.ics&lt;/code&gt; event with a 15-minute alarm), &lt;strong&gt;save a pocket card&lt;/strong&gt; (it opens with no signal), and &lt;strong&gt;I'm going&lt;/strong&gt;, which turns the whole screen green and tells you to close the tab.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you come back, it asks "Did you go?" and counts your minutes outside for the week, on your device only.&lt;/p&gt;

&lt;p&gt;It's for anyone who means to go outside and then doesn't: people at desks, dog owners, the "I'll garden this weekend" crowd, and birders who want the morning when the wind is down.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://out-the-door-jade.vercel.app" rel="noopener noreferrer"&gt;https://out-the-door-jade.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a real run from today in Seattle with Gemma 4 E2B. I typed &lt;em&gt;"take the pup around the lake once I log off at 5:30, I don't want to get rained on"&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhvg3zndnb47qveyjly91.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhvg3zndnb47qveyjly91.png" alt="Out the Door result: read as Walk, 45 min, today, after 5:30 PM, avoid rain. Best window Today 6 PM to 6:45 PM, great 100/100, with Gemma's note and the three buttons" width="655" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gemma read it as a 45-minute walk, today, after 5:30 PM, avoiding rain. (45 minutes is the default walk length, since I didn't give one.) The forecast was dry and calm, so the best window was &lt;strong&gt;6:00â€“6:45 PM&lt;/strong&gt;, and because that ends 5 minutes after the 6:40 PM sunset, the note says to bring a headlamp. Gemma wrote: &lt;em&gt;"Leave at 6 PM and be back by 6:45 PM. Bring a headlamp since you finish near sunset. Look for the light as you walk."&lt;/em&gt; The code checked every time and number in it against the forecast.&lt;/p&gt;

&lt;p&gt;The public demo works without installing anything in "No model" mode (simple keyword rules), and there's an experimental in-browser Gemma 3 1B. The full version is Gemma 4 E2B on your own laptop through Ollama:&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 gemma4:e2b
&lt;span class="nv"&gt;OLLAMA_ORIGINS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://out-the-door-jade.vercel.app"&lt;/span&gt; ollama serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then pick "Gemma 4 via Ollama" in the app and click &lt;strong&gt;Load Gemma&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Zhuoli" rel="noopener noreferrer"&gt;
        Zhuoli
      &lt;/a&gt; / &lt;a href="https://github.com/Zhuoli/out-the-door" rel="noopener noreferrer"&gt;
        out-the-door
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Find the hour to go outside, then close the tab. Gemma reads your plan on your own device; code picks the window from an open forecast. DEV Hacktoberfest 2026 Week 1: Touch Grass.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🌿 Out the Door&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Find the hour to go outside, then close the tab.&lt;/strong&gt; Say what you want to do in plain words ("45 min walk with the dog before dark, I hate wind"). &lt;strong&gt;Gemma, running on your own device&lt;/strong&gt;, turns that into a plan; code scores the free &lt;a href="https://open-meteo.com/" rel="nofollow noopener noreferrer"&gt;Open-Meteo&lt;/a&gt; forecast and picks the best window; you get a calendar reminder and a pocket card that still opens with no signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://out-the-door-jade.vercel.app" rel="nofollow noopener noreferrer"&gt;https://out-the-door-jade.vercel.app&lt;/a&gt; · Built for the DEV Hacktoberfest 2026 Open-Source AI Challenge, Week 1: &lt;em&gt;Touch Grass&lt;/em&gt;. Repo started Oct 5, 2026.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;The screen should be the shortest part of going outside. Weather apps make you read 72 hourly rows and do the math yourself ("is 40% at 3 PM worse than 15 km/h wind at 5?"), and by then you've spent ten minutes on your phone and it's dark. Out the Door asks one question, gives…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Zhuoli/out-the-door" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


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

&lt;p&gt;It's a static site: HTML, ES modules, a service worker, and no backend. One rule shaped the code: &lt;strong&gt;Gemma does the language and code does the arithmetic, and code checks everything Gemma writes.&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;Job&lt;/th&gt;
&lt;th&gt;Who does it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read "after I log off at 5:30, take the pup around the lake" into JSON: &lt;code&gt;{activity, durationMin, earliest, latest, days, avoid}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Gemma&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check and repair that JSON: unknown activity becomes walk, bad times are dropped, durations are clamped, "stargazing but avoid the dark" is fixed. Every repair is shown to you&lt;/td&gt;
&lt;td&gt;code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Score every hour and pick non-overlapping windows. A window scores 0.6 Ã— its average hour plus 0.4 Ã— its worst hour, so one soggy hour sinks it. Birding gets a dawn bonus, foliage a golden-hour bonus, and sooner beats later&lt;/td&gt;
&lt;td&gt;code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If nothing good fits your limits, offer the best time outside them (a Phoenix "run after work" in a 36 Â°C afternoon moves to 6 AM)&lt;/td&gt;
&lt;td&gt;code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What to bring&lt;/td&gt;
&lt;td&gt;code, from the numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The door note: when to leave, what to bring, one thing to notice&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Gemma&lt;/strong&gt;, given only that window's facts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check the note: every clock time must be the window's start, end or sunset, every number must be in the forecast, and no "rain" in a dry window. If it fails, you see a plain note first, with Gemma's version and the reason underneath&lt;/td&gt;
&lt;td&gt;code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The open pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; (&lt;code&gt;gemma4:e2b&lt;/code&gt;, Q4_K_M) through &lt;strong&gt;Ollama&lt;/strong&gt;, on your own machine. It reads your request and writes the note.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 1B&lt;/strong&gt; (&lt;code&gt;onnx-community/gemma-3-1b-it-ONNX&lt;/code&gt;) through &lt;strong&gt;Transformers.js&lt;/strong&gt; in a Web Worker (WebGPU, or WebAssembly on CPU). It's a one-time 1 GB download, then cached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-Meteo&lt;/strong&gt; for the forecast and town search: open data, free, no API key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  I measured it, and the numbers changed the design
&lt;/h3&gt;

&lt;p&gt;I wrote two hand-labeled sets of requests. The first 20 I wrote while building, alongside a keyword-rules baseline. The other 12 I wrote &lt;em&gt;after&lt;/em&gt; freezing the rules and the prompt, phrased the way people actually talk: "stretch my legs for twenty minutes", "I burn easily", "I melt above 20 degrees". A request only counts if all six fields are right.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;th&gt;Dev set (20)&lt;/th&gt;
&lt;th&gt;Held-out set (12)&lt;/th&gt;
&lt;th&gt;Avg per request (CPU)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keyword rules, no model&lt;/td&gt;
&lt;td&gt;15/20&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/12&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; via Ollama&lt;/td&gt;
&lt;td&gt;13/20&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7/12&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5â€“6.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 1B in Transformers.js&lt;/td&gt;
&lt;td&gt;0/20&lt;/td&gt;
&lt;td&gt;0/12&lt;/td&gt;
&lt;td&gt;3.8â€“4.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things I learned:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;My keyword rules were overfit to my own examples.&lt;/strong&gt; 15/20 on the set I wrote them against and 0/12 on new phrasing. "Twenty minutes" has no digit, "I burn easily" means avoid sun, and "tramp through the woods" is a hike. On the held-out set, Gemma 4 E2B got the activity, the duration and both time limits right on &lt;strong&gt;12 of 12&lt;/strong&gt;. Its misses were judgment calls: it read "once I log off at 5:30" as &lt;em&gt;today&lt;/em&gt;, and it missed "avoid wind" in "I don't want to get blown around on the bike".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first prompt let Gemma invent limits.&lt;/strong&gt; On version 1 it scored 12/20 on the dev set and turned "leaf peeping, back before dark" into 17:00â€“21:00. Telling it "use null unless they named a time of day", and that "before dark" is something to &lt;em&gt;avoid&lt;/em&gt; rather than a time limit, plus mapping 00:00â€“23:59 to "no limit" in code, got it to 13/20, and the invented limits went away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1B was too small for this job.&lt;/strong&gt; Gemma 3 1B copied the example's &lt;code&gt;avoid&lt;/code&gt; list into almost every answer. I tried three prompt variants (one example, no example, everything in a single user turn) and all scored 0/20 and 0/12. So in the browser it only writes the note, and the keyword rules do the reading. I'd rather ship that honestly than pretend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The notes, against six real forecasts I saved on October 5 (sunny Seattle, Bergen in steady rain, Phoenix in a heat spell):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 E2B:&lt;/strong&gt; 6/6 passed the code check, and when I read them by hand they were all accurate but plain ("Look for the low golden light before sunset.").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 1B:&lt;/strong&gt; 4/6 passed. The check caught "rain-soaked streets" in a dry window and a note that never said when to leave. It &lt;em&gt;can't&lt;/em&gt; catch nature facts: 1B called a Pacific Wren "iridescent blue" and sent a Phoenix runner "through the redwoods". That's why code chooses the window and only Gemma's wording is ever at risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All raw outputs, misses included, are in &lt;a href="https://github.com/Zhuoli/out-the-door/tree/main/results" rel="noopener noreferrer"&gt;&lt;code&gt;results/&lt;/code&gt;&lt;/a&gt;. There are also 12 unit tests covering parsing and repair, scoring, daylight, overlap, the note checker and the &lt;code&gt;.ics&lt;/code&gt; time-zone math.&lt;/p&gt;

&lt;p&gt;I built this with an AI coding agent pairing with me. The design calls, the labeled test sets and the decision to demote the 1B model came from the measurements above.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Where I'm going stays on my computer.&lt;/strong&gt; "Take the pup around the lake once I log off at 5:30" is a routine, a schedule and a place someone lives near. With an open-weight model on my own laptop, the only thing that leaves my device is a coordinate sent to a free forecast API. There's no account, no analytics and no server of mine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run, so it can stay simple.&lt;/strong&gt; There's no per-token bill and no key to hide, so it's a static page anyone can fork and host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could swap models and measure them.&lt;/strong&gt; The most useful thing I did was run the same prompts through Gemma 4 E2B and Gemma 3 1B and compare them against a no-model baseline on requests I hadn't tuned for. That's how I found out my rules were overfit and the 1B model wasn't ready for this job. With a closed API I'd have one model, a version that can change under me, and no way to run it offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pocket card works with no signal.&lt;/strong&gt; The plan, the forecast it came from and the app itself are cached on the device, so the screen part ends before the trailhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I haven't taken it on a real trail yet. When I do, I'll add what I learn to the README.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 4 E2B via Ollama reads the request and writes the door note, with Gemma 3 1B in the browser via Transformers.js as an experimental mode. Measured against a no-model baseline on held-out requests.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Lease Buddy: a private lease checker for a friend, with Gemma running on their own laptop</title>
      <dc:creator>Zhuoli Liang</dc:creator>
      <pubDate>Sat, 03 Oct 2026 09:45:09 +0000</pubDate>
      <link>https://dev.to/zhuoli_liang_b6b05a8c3c1c/lease-buddy-a-private-lease-checker-for-a-friend-with-gemma-running-on-their-own-laptop-3f8e</link>
      <guid>https://dev.to/zhuoli_liang_b6b05a8c3c1c/lease-buddy-a-private-lease-checker-for-a-friend-with-gemma-running-on-their-own-laptop-3f8e</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built this for a friend who is about to sign a 12-month apartment lease: pages of dense legal English, plus an addendum. I can read it with them once, but I won't be there for the next lease, or the renewal letter. And a lease is exactly the kind of document you shouldn't paste into a random website: name, address, rent, signature.&lt;/p&gt;

&lt;p&gt;So I built them &lt;strong&gt;Lease Buddy&lt;/strong&gt;: paste a lease (or open a &lt;code&gt;.txt&lt;/code&gt; / &lt;code&gt;.pdf&lt;/code&gt;), and it gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The short version:&lt;/strong&gt; rent, due date, late fee, deposit, start and end dates, notice periods, early-termination cost. Every item cites its clause (&lt;code&gt;§3&lt;/code&gt;, &lt;code&gt;§13&lt;/code&gt;…).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clauses that contradict each other.&lt;/strong&gt; This is the part I care about most. In real leases the scary stuff usually isn't hidden; it's &lt;em&gt;scattered&lt;/em&gt;. One clause says you need 60 days' notice to avoid auto-renewal, another says 30 days to vacate, and an addendum quietly moves the late-rent cutoff from the 5th to the 3rd.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dates you can't miss&lt;/strong&gt;, computed rather than guessed, with a &lt;code&gt;.ics&lt;/code&gt; export and 7-day reminders. When two clauses disagree, it uses the earlier (safer) date and shows both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions to send the landlord:&lt;/strong&gt; each flag (auto-renewal, non-refundable fees, "reasonable notice" with no number, "tenant pays any repair under $250 regardless of cause", one-sided attorney fees) becomes a copy-paste question rather than legal advice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask Gemma.&lt;/strong&gt; "Can my landlord come in whenever they want?" or "Explain §13 in plain words." Gemma runs &lt;strong&gt;in the browser tab or on your own computer&lt;/strong&gt;, never on a server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgrig1k6dxo4z6s0z041u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgrig1k6dxo4z6s0z041u.png" alt="Contradictions found in the sample lease" width="800" height="638"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://lease-buddy-gemma.vercel.app" rel="noopener noreferrer"&gt;https://lease-buddy-gemma.vercel.app&lt;/a&gt;. Click &lt;strong&gt;"Try the sample lease"&lt;/strong&gt; (a fictional lease with three contradictions planted on purpose). The rule checks are instant and need no model. Then hit &lt;strong&gt;Start Gemma&lt;/strong&gt; to ask questions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa5orlddh20d259k7druw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa5orlddh20d259k7druw.png" alt="Key terms with clause citations" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fueyspzpcn3a9xuqh5ksm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fueyspzpcn3a9xuqh5ksm.png" alt="Dates you can't miss, with an .ics export" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's Gemma 4 (E2B, via Ollama on my dev box) answering the question that matters most in this lease. The yellow line comes from the rule checker, not the model. The green line is the code verifying Gemma's answer:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhf9go4mgz05d46jas95i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhf9go4mgz05d46jas95i.png" alt="Gemma 4 answering with both contradictory clauses cited" width="800" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

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


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Zhuoli" rel="noopener noreferrer"&gt;
        Zhuoli
      &lt;/a&gt; / &lt;a href="https://github.com/Zhuoli/lease-buddy" rel="noopener noreferrer"&gt;
        lease-buddy
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Read your lease like a friend who has read a lot of leases. Finds contradictory clauses + key dates; Gemma explains them on your own device.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🏠 Lease Buddy&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Read your lease like a friend who has read a lot of leases.&lt;/strong&gt; Paste a lease (or open a .txt/.pdf). Lease Buddy finds the money, the dates, and the clauses that &lt;strong&gt;contradict each other&lt;/strong&gt;. Then &lt;strong&gt;Gemma, running on your own device&lt;/strong&gt;, explains them in plain words.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://lease-buddy-gemma.vercel.app" rel="nofollow noopener noreferrer"&gt;https://lease-buddy-gemma.vercel.app&lt;/a&gt; · Built for the DEV Hacktoberfest 2026 Weekend Challenge ("Build for a Friend").&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Not legal advice. Lease Buddy points at things to ask about. A local tenant-rights group or a lawyer can tell you what the law says where you live.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;I built this for a friend who is about to sign a 12-month lease written in dense legal English. The scary parts of a lease are rarely hidden. They're &lt;em&gt;scattered&lt;/em&gt;: the renewal clause says one notice period, the "notice to vacate" clause says another, and an addendum quietly changes when rent counts as late…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Zhuoli/lease-buddy" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Plain HTML + ES modules, no build step, no backend, no API keys. Apache-2.0. &lt;code&gt;npm test&lt;/code&gt; runs 25 unit tests (clause splitting, fact extraction, contradiction detection, date math, retrieval, the answer verifier, &lt;code&gt;.ics&lt;/code&gt;).&lt;/p&gt;

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

&lt;p&gt;I built it this weekend with an AI coding agent doing a lot of the typing, while I made the calls on scope, tests and the parts below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The split that made it work: code finds the facts, Gemma explains them.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic layer (&lt;code&gt;src/lease.js&lt;/code&gt;):&lt;/strong&gt; splits the lease into clauses (including addendum ids like &lt;code&gt;A2&lt;/code&gt;), reads number words ("sixty (60) days"), and pulls out typed facts like &lt;code&gt;end_of_term_notice = 60 days @ §13&lt;/code&gt;. If two clauses give different values for the same thing, that's a contradiction. Dates are plain date math in UTC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval:&lt;/strong&gt; a tiny BM25 over clauses, plus a few tenant-language synonyms ("moving out" → vacate / non-renewal). It's &lt;strong&gt;contradiction-aware&lt;/strong&gt;: if a retrieved clause is part of a contradiction, the clause that contradicts it gets pulled in too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma&lt;/strong&gt; only sees those clauses, plus a note from the checker listing the contradiction, and is told to cite &lt;code&gt;[§x]&lt;/code&gt; after each fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifier:&lt;/strong&gt; every number in Gemma's answer must appear in a clause it was shown (and in the clause cited in &lt;em&gt;that sentence&lt;/em&gt;), every &lt;code&gt;[§x]&lt;/code&gt; must be real, and if a contradiction is in play, both sides must be mentioned. Otherwise the answer gets a ⚠️ that says exactly what's off.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Two ways to run Gemma, both on hardware you control:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In the browser:&lt;/strong&gt; &lt;code&gt;gemma-3-1b-it&lt;/code&gt; (ONNX) via &lt;strong&gt;Transformers.js&lt;/strong&gt; in a Web Worker. WebGPU (q4f16, or q4 when the GPU lacks fp16 shaders) with a CPU/WASM fallback (int8). About a 0.8–1 GB one-time download, then cached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama on your computer:&lt;/strong&gt; &lt;code&gt;ollama pull gemma4:e2b&lt;/code&gt;, allow the site's origin with &lt;code&gt;OLLAMA_ORIGINS&lt;/code&gt;, and the page streams from &lt;code&gt;localhost:11434&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;I measured it instead of eyeballing it.&lt;/strong&gt; &lt;code&gt;npm run gemma:check&lt;/code&gt; runs the real prompts against the sample lease and saves every answer to &lt;code&gt;results/&lt;/code&gt; (all on CPU):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Q&amp;amp;A answers passing the checker&lt;/th&gt;
&lt;th&gt;Clause explanations passing&lt;/th&gt;
&lt;th&gt;Same 3 contradiction questions &lt;em&gt;without&lt;/em&gt; the checker's hint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E2B (Ollama)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7/7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 1B (the in-browser model)&lt;/td&gt;
&lt;td&gt;5/7&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;td&gt;1/3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things I learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small models answer from one clause.&lt;/strong&gt; Without the hint, Gemma 4 answered "how much notice before moving out?" with &lt;em&gt;"at least thirty (30) days' written notice [§14]"&lt;/em&gt;. That's true, but it skipped §13's 60-day non-renewal deadline, which is the one that locks you into another year. The system prompt already said "if two clauses disagree, say so and cite both", and that wasn't enough. When the checker's note naming the specific contradiction was added, it cited both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The verifier earns its keep on the 1B model.&lt;/strong&gt; It caught Gemma 3 1B citing numbers to the wrong clause ("thirty days … [§13]"), inventing a clause id (&lt;code&gt;§A9&lt;/code&gt;), and turning "sixty (60) days" into "six months".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can't check meaning&lt;/strong&gt;, and I'd rather say so. 1B once said "the lease doesn't allow you to bring a cat" when the clause says pets need written consent. That's why each answer shows the exact clauses Gemma saw, why the summary, contradictions and dates come from code, and why the app labels the 1B option "small, sometimes cites the wrong clause" and recommends Gemma 4 for answers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also: the first in-browser run crashed with &lt;em&gt;"Program Sub requires f16 but the device does not support it"&lt;/em&gt;. Lots of GPUs don't expose &lt;code&gt;shader-f16&lt;/code&gt;, so the worker now checks the adapter, picks &lt;code&gt;q4&lt;/code&gt; instead of &lt;code&gt;q4f16&lt;/code&gt;, runs a 2-token smoke test, and falls back to the CPU if the GPU path still fails.&lt;/p&gt;

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

&lt;p&gt;Because of what a lease &lt;em&gt;is&lt;/em&gt;: legal name, address, rent, sometimes income and a signature. Uploading that to someone else's server to get it explained is exactly the wrong move. With an open-weight model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The document never leaves the device.&lt;/strong&gt; The site is static files on Vercel. Gemma runs in the tab (Transformers.js) or on your own machine (Ollama). There's no server I could log to, even by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run&lt;/strong&gt;, for my friend or for me. No API key, no per-token bill, no account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could pick the model size to match the job.&lt;/strong&gt; A 1B model that loads in a browser for a quick private look, or Gemma 4 E2B on a laptop when you want answers you can lean on. The same prompts and the same verifier work for both, and I could measure both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The open stack let me fix the hard part.&lt;/strong&gt; Swapping models, reading their failure modes in &lt;code&gt;results/&lt;/code&gt;, and adding a verifier around them is how the "answers from one clause" problem got solved. A closed API would have given me a nicer-sounding answer with the same blind spot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not legal advice: Lease Buddy tells you what to &lt;em&gt;ask&lt;/em&gt;. For what the law says where you live, a local tenant-rights group is the right call. Next step: going through my friend's real lease with them.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 3 1B runs fully in the browser via Transformers.js (WebGPU/WASM), and Gemma 4 E2B runs locally through Ollama. Both are wrapped in contradiction-aware retrieval and a code verifier, with a measured comparison in the repo.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
    </item>
    <item>
      <title>Fine Print: an agent that knows when the FAQ and the Official Rules disagree</title>
      <dc:creator>Zhuoli Liang</dc:creator>
      <pubDate>Sat, 03 Oct 2026 07:51:06 +0000</pubDate>
      <link>https://dev.to/zhuoli_liang_b6b05a8c3c1c/fine-print-an-agent-that-knows-when-the-faq-and-the-official-rules-disagree-4pml</link>
      <guid>https://dev.to/zhuoli_liang_b6b05a8c3c1c/fine-print-an-agent-that-knows-when-the-faq-and-the-official-rules-disagree-4pml</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path One: Ship an Agent That Queries Real Content&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Fine Print&lt;/strong&gt; reads contest rules so you don't miss the clause that disqualifies you.&lt;/p&gt;

&lt;p&gt;I enter a lot of hackathons and bounties. The rules for one contest are never on one page. There's a landing page, an FAQ, a contest-rules page, and the platform's general Official Rules, and they drift apart. This very challenge is a good example. Its own pages disagree in ways that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When does the winner paperwork clock start?&lt;/strong&gt; The FAQ gives you 7 business days "following the date of your first email notification". The MLH Official Rules say "following the date of first &lt;em&gt;attempted&lt;/em&gt; notification", and add: "Failure to comply with these deadlines may result in forfeiture of the prize." Under the Official Rules, an email sitting in your spam folder has already started the clock. And the Official Rules govern: &lt;em&gt;"In the event of a conflict between these Official Rules and the applicable Contest Announcement Page, the Official Rules will govern and control."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who is "Sponsor"?&lt;/strong&gt; The landing page says "Sponsored by Sanity". The contest-rules page says "Sponsored by Major League Hacking PBC Inc ("Sponsor")". The employee exclusion is written against &lt;em&gt;Sponsor&lt;/em&gt;. Can a Sanity employee enter? The rules don't say. Fine Print says exactly that and tells you to ask, instead of guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can I reuse a project?&lt;/strong&gt; The FAQ says riffing on prior work is encouraged. The Official Rules require that "development of your Entry was started during, and not prior to, the Entry Period".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Submissions due: October 04, 2026"&lt;/strong&gt; is 2:59 PM on &lt;strong&gt;October 5&lt;/strong&gt; in Beijing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tell Fine Print who you are (country, time zone, age, employer, whether you use AI, whether an agent submits for you). Then ask a plain question. The offline results include clause IDs and a tool trace with source quotes and URLs. Model-generated citation correctness is not yet automatically verified.&lt;/p&gt;

&lt;p&gt;The local snapshot covers three contests: this one, the DEV Kaggle Benchmarking Challenge, and a Superteam Earn bounty (which turns out to be &lt;code&gt;HUMAN_ONLY&lt;/code&gt;, so an agent can draft for you but not submit).&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://fine-print-opal.vercel.app" rel="noopener noreferrer"&gt;https://fine-print-opal.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Shareable demo questions:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://fine-print-opal.vercel.app/?p=Sanity%20employee&amp;amp;q=Can%20I%20enter%20the%20Sanity%20challenge%2C%20and%20what%20is%20the%20catch%3F" rel="noopener noreferrer"&gt;https://fine-print-opal.vercel.app/?p=Sanity%20employee&amp;amp;q=Can%20I%20enter%20the%20Sanity%20challenge%2C%20and%20what%20is%20the%20catch%3F&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fine-print-opal.vercel.app/?p=Dev%20in%20Beijing%2C%20old%20project&amp;amp;q=When%20exactly%20does%20the%20Sanity%20challenge%20close%20for%20me%3F" rel="noopener noreferrer"&gt;https://fine-print-opal.vercel.app/?p=Dev%20in%20Beijing%2C%20old%20project&amp;amp;q=When%20exactly%20does%20the%20Sanity%20challenge%20close%20for%20me%3F&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fine-print-opal.vercel.app/?q=If%20I%20win%20the%20Sanity%20challenge%2C%20when%20does%20the%20cash%20actually%20land%3F" rel="noopener noreferrer"&gt;https://fine-print-opal.vercel.app/?q=If%20I%20win%20the%20Sanity%20challenge%2C%20when%20does%20the%20cash%20actually%20land%3F&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Studio:&lt;/strong&gt; &lt;a href="https://fine-print.sanity.studio/" rel="noopener noreferrer"&gt;https://fine-print.sanity.studio/&lt;/a&gt; (sign-in required; the &lt;strong&gt;⚠️ Open conflicts&lt;/strong&gt; desk item is the review queue)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm2mbosann4zfsy6g4zp9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm2mbosann4zfsy6g4zp9.png" alt="Fine Print answering for a Sanity employee" width="800" height="1267"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/Zhuoli/fine-print" rel="noopener noreferrer"&gt;https://github.com/Zhuoli/fine-print&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clone it and run &lt;code&gt;npm install &amp;amp;&amp;amp; npm run seed:build &amp;amp;&amp;amp; npm run dev&lt;/code&gt;. It works with &lt;strong&gt;no accounts at all&lt;/strong&gt;: GROQ runs locally via groq-js over the same seed, the Context tools fall back to same-named local shims, and with no LLM key a deterministic planner calls the same tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The content model is the product.&lt;/strong&gt; Rules are prose, but what an entrant needs is computation: comparing instants, ranking sources, checking a country against a list. So every rule lives in Sanity twice: once &lt;strong&gt;verbatim&lt;/strong&gt; (for citation), once &lt;strong&gt;normalized&lt;/strong&gt; (for code).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ruleSource&lt;/code&gt;: a page, its &lt;code&gt;kind&lt;/code&gt; (officialRules / contestRules / landingPage / listing), a &lt;strong&gt;&lt;code&gt;precedenceRank&lt;/code&gt;&lt;/strong&gt;, and the exact sentence that establishes precedence.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clause&lt;/code&gt;: a &lt;code&gt;topic&lt;/code&gt;, a &lt;strong&gt;verbatim &lt;code&gt;quote&lt;/code&gt;&lt;/strong&gt;, a &lt;code&gt;normalized&lt;/code&gt; JSON value (&lt;code&gt;{"minAge":18}&lt;/code&gt;, &lt;code&gt;{"instant":"2026-10-05T06:59:00Z"}&lt;/code&gt;, &lt;code&gt;{"excludedCountries":[…]}&lt;/code&gt;), a reference to its &lt;code&gt;source&lt;/code&gt;, and &lt;code&gt;contests[]&lt;/code&gt;. One clause in the shared MLH Official Rules binds every DEV contest, so I fix it once.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;conflict&lt;/code&gt;: two or more &lt;code&gt;claims[]&lt;/code&gt; (references to clauses) side by side, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;winningClaim&lt;/code&gt;, &lt;code&gt;resolvedBy&lt;/code&gt; (&lt;code&gt;precedence&lt;/code&gt; / &lt;code&gt;precision&lt;/code&gt; / &lt;code&gt;needsHuman&lt;/code&gt;), and a &lt;code&gt;rationale&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;contest&lt;/code&gt;: the close &lt;strong&gt;instant&lt;/strong&gt; and the close &lt;strong&gt;as written&lt;/strong&gt;, the source time zone, tracks with winners per path, and a payout timeline (business-day windows with their anchors).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;organizer&lt;/code&gt;: &lt;strong&gt;legal sponsor vs. brand sponsor&lt;/strong&gt;, because "Sponsor" in the rules isn't the logo on the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the Studio, the first item in the desk structure is &lt;strong&gt;⚠️ Open conflicts&lt;/strong&gt;, a review queue of contradictions nobody has decided yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sanity Context: one endpoint over the full dataset (no Knowledge Base).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The live app is wired to the project-level Sanity Context MCP endpoint for the whole &lt;code&gt;production&lt;/code&gt; dataset, in GROQ mode with &lt;strong&gt;dataset embeddings enabled&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;https://api.sanity.io/v2026-03-03/context/mcp/9wt4tu94/production?embeddings=true&lt;/code&gt;.&lt;br&gt;
The server connects to it with a read-only Viewer token kept in a server-side environment variable. &lt;code&gt;tools/list&lt;/code&gt; returns &lt;code&gt;initial_context&lt;/code&gt;, &lt;code&gt;groq_query&lt;/code&gt;, &lt;code&gt;schema_explorer&lt;/code&gt;, and &lt;code&gt;array_field_reader&lt;/code&gt;. Embeddings cover { title, name, topic, quote, note, rationale, closesAsWritten }. So semantic ranking works through the MCP: &lt;code&gt;*[_type=="clause"] | score(text::semanticSimilarity("when is the prize money paid"))&lt;/code&gt; ranks &lt;code&gt;clause.sanity.prize.rules&lt;/code&gt;, &lt;code&gt;clause.tg.payout&lt;/code&gt;, and &lt;code&gt;clause.official.notify&lt;/code&gt; first. With an LLM configured, the agent gets the schema overview from &lt;code&gt;initial_context&lt;/code&gt; and uses &lt;code&gt;groq_query&lt;/code&gt; to follow references. A typical call is one contest, its sources ordered by precedence, every clause that &lt;code&gt;references()&lt;/code&gt; it, and every conflict with its claims dereferenced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About Knowledge Bases, honestly:&lt;/strong&gt; the Knowledge Bases option isn't shown in the dashboard for my Free-plan project, so I couldn't build one. The challenge page says: "Knowledge Bases are in beta and currently index up to 150 documents. Design for that budget, or point your agent at your full dataset through a Context MCP endpoint with embeddings enabled. Both count for Path One." Fine Print takes the second option. &lt;code&gt;kb/knowledge-base.md&lt;/code&gt; holds the KB setup (dataset query, website sources, file bundle, standing instructions) that I'd apply once access is available. The app has a &lt;code&gt;knowledge_base_read&lt;/code&gt; slot that currently falls back to a local shim over the verbatim snapshots in &lt;code&gt;/sources&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the public demo actually runs:&lt;/strong&gt; the deployment has no paid LLM key. So the live demo uses the deterministic planner. It calls the same tools, and they run GROQ directly against the live Content Lake (&lt;code&gt;9wt4tu94/production&lt;/code&gt;). The Context MCP is configured and verified, but the planner doesn't call the MCP tools itself. Add &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; or &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; and the model plans over the MCP tools instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the agent does with it.&lt;/strong&gt; The model is never allowed to do date math or eligibility logic in its head. It calls five deterministic tools: &lt;code&gt;check_deadline&lt;/code&gt;, &lt;code&gt;check_eligibility&lt;/code&gt;, &lt;code&gt;cash_timeline&lt;/code&gt;, &lt;code&gt;submission_checklist&lt;/code&gt;, and &lt;code&gt;source_conflicts&lt;/code&gt;. Each runs GROQ against Sanity and returns verdicts carrying clause ids. The system prompt requires a &lt;code&gt;[clause.*]&lt;/code&gt; citation on every factual sentence, and requires the agent to say "the rules don't settle this" whenever a conflict is &lt;code&gt;open&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A keyword search would return "October 04" and "seven (7) business days". Answering "I'm in Beijing, when must I submit, and if I win, when is my W-9 due and when does the money arrive?" takes the instant, the zone, the precedence ranks, and the anchored business-day windows. Fine Print converts the submission instant to Beijing time, but refuses to invent a cash date: winner selection is not recorded, paperwork is due 7 business days after first attempted notification, and delivery depends on acknowledged acceptance of completed paperwork.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conflicts are modeled by hand (no KB build)
&lt;/h3&gt;

&lt;p&gt;Without a Knowledge Base build, there are no machine-raised Issues to compare. The five &lt;code&gt;conflict&lt;/code&gt; documents in the dataset are modeled by hand, each with side-by-side claims, a winning claim, and a rationale. Comparing them with a KB build's Issues is the first thing I'd do once access is available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Honest limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The seed is curated: three contests and about 30 clauses. Extracting clauses from a new rules page is still a manual (AI-assisted) step. The next step is a Sanity Function that drafts &lt;code&gt;clause&lt;/code&gt; documents from a new &lt;code&gt;ruleSource&lt;/code&gt; for a human to approve.&lt;/li&gt;
&lt;li&gt;Business-day math skips weekends but not public holidays.&lt;/li&gt;
&lt;li&gt;The Kaggle challenge's landing page gives a date with no time. Fine Print says the time is unknown instead of assuming 11:59 PM PDT.&lt;/li&gt;
&lt;li&gt;This isn't legal advice. Where the rules are silent, the agent says so.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project ID:&lt;/strong&gt; &lt;code&gt;9wt4tu94&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset:&lt;/strong&gt; &lt;code&gt;production&lt;/code&gt; (60 documents, imported from &lt;code&gt;seed/production.ndjson&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Studio:&lt;/strong&gt; &lt;a href="https://fine-print.sanity.studio/" rel="noopener noreferrer"&gt;https://fine-print.sanity.studio/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context MCP:&lt;/strong&gt; &lt;code&gt;https://api.sanity.io/v2026-03-03/context/mcp/9wt4tu94/production?embeddings=true&lt;/code&gt; (dataset embeddings enabled; needs a read token)&lt;/li&gt;
&lt;li&gt;The dataset's visibility is public, but document IDs are dotted (&lt;code&gt;clause.official.clock&lt;/code&gt;), and Sanity doesn't serve those to anonymous queries. So the app reads with a server-side Viewer token. To browse without an account, use &lt;code&gt;npm run seed:build&lt;/code&gt; and read &lt;code&gt;seed/production.ndjson&lt;/code&gt;, which is the exact imported content.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reproducible offline evidence
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;npm test&lt;/code&gt; with no credentials. It checks deadline boundaries, unknown eligibility, request validation, payout-anchor uncertainty, seed references, and a counterfactual: keeping the quoted prose unchanged while changing the normalized minimum age changes the verdict. This demonstrates dependence on structured facts, but is not evidence of a live Context call. &lt;code&gt;npm run check:mcp&lt;/code&gt; checks the live Context endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this with an AI coding agent. I wrote the brief and the content model decisions, and checked every quoted clause against the source pages. The agent wrote most of the code.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
    </item>
  </channel>
</rss>
