<?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>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>
