<?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: Kateryna Ivashchenko</title>
    <description>The latest articles on DEV Community by Kateryna Ivashchenko (@rayyer).</description>
    <link>https://dev.to/rayyer</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%2F4103377%2F10a07fea-b148-4e18-b97a-a1ac61902894.png</url>
      <title>DEV Community: Kateryna Ivashchenko</title>
      <link>https://dev.to/rayyer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rayyer"/>
    <language>en</language>
    <item>
      <title>Clockwrit — every hour has a citation (an agent that knows what time it is, legally)</title>
      <dc:creator>Kateryna Ivashchenko</dc:creator>
      <pubDate>Sun, 04 Oct 2026 22:07:23 +0000</pubDate>
      <link>https://dev.to/rayyer/clockwrit-every-hour-has-a-citation-an-agent-that-knows-what-time-it-is-legally-5ee3</link>
      <guid>https://dev.to/rayyer/clockwrit-every-hour-has-a-citation-an-agent-that-knows-what-time-it-is-legally-5ee3</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;On &lt;strong&gt;1 November 2026, Canada stops changing its clocks&lt;/strong&gt; — one province at a time. British Columbia, Alberta and the Northwest Territories already went permanent this year; Manitoba follows on 31 October. IANA shipped five tzdata releases in 2026 to keep up, and for several of these changes it &lt;em&gt;deliberately&lt;/em&gt; models the switch on 1 November at 02:00 instead of the legal date (a documented "temporary hack"). Morocco quietly went back to GMT on 20 September.&lt;/p&gt;

&lt;p&gt;Meanwhile, the Node.js you're running ships whatever tz data it was built with. On my machine that's 2025b: &lt;strong&gt;11 of 597 zones are wrong&lt;/strong&gt; in 2026–27. Even the Vercel runtime this project is deployed on (tzdata 2026c) still puts Winnipeg an hour off. Ask a search engine whether Ukraine abolished daylight saving time and you'll get "yes" — the bill passed in 2024 and was never signed.&lt;/p&gt;

&lt;p&gt;So I built an agent that answers &lt;em&gt;what the clock and the calendar legally say&lt;/em&gt; at a place and moment — and on whose authority.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clockwrit&lt;/strong&gt; reads the law, the IANA time zone database and your runtime's own clock, and tells you which one is right — with the decree, the date and the source quote.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ask.&lt;/strong&gt; "A weekly call is set for Mondays 10:00 in New York. What time is it in Winnipeg on 2 November 2026, and why?" The answer comes back as cards, not prose: the local time, and &lt;strong&gt;four clocks side by side — the law, IANA 2026e, this server, and &lt;em&gt;your browser&lt;/em&gt;, read live&lt;/strong&gt; — plus every instrument the answer rests on, with its authority tier (§ primary law, † reference data, ‡ community) and its legal status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pending laws as Content Releases.&lt;/strong&gt; The US Sunshine Protection Act, Ukraine's bill 4201, Florida's conditional permanent-DST law, the EU proposal and three more are modelled as Sanity Content Releases. Pick one and the agent answers &lt;em&gt;as if it had passed&lt;/em&gt; — the same tools, reading the dataset through the release perspective.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A ruling desk.&lt;/strong&gt; When the Knowledge Base finds two sources that disagree, the agent drafts a ruling, a person signs it, and the decision becomes a standing instruction for every future rebuild. (More below — this is the part I'm proudest of.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A schedule auditor&lt;/strong&gt;, &lt;strong&gt;a drift report&lt;/strong&gt;, &lt;strong&gt;an eval page&lt;/strong&gt;, &lt;strong&gt;an MCP server&lt;/strong&gt;, &lt;strong&gt;a CLI&lt;/strong&gt; and &lt;strong&gt;a Sanity Dashboard app&lt;/strong&gt;.&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%2F2cry9p87qzo5qi5higgr.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%2F2cry9p87qzo5qi5higgr.png" alt="Ask: a real answer with the four clocks and the legal basis" width="800" height="977"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/pSU875tMuzw" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://clockwrit.vercel.app" rel="noopener noreferrer"&gt;https://clockwrit.vercel.app&lt;/a&gt; — no login needed.&lt;/p&gt;

&lt;p&gt;A five-minute path, if you're reviewing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Home&lt;/strong&gt; — the page reads &lt;em&gt;your&lt;/em&gt; browser's tz data for 2 November and grades it against the law (&lt;a href="https://clockwrit.vercel.app" rel="noopener noreferrer"&gt;landing&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://clockwrit.vercel.app/ask" rel="noopener noreferrer"&gt;Ask&lt;/a&gt;&lt;/strong&gt; — try a suggested question, then switch "Answer under" to &lt;em&gt;Ukraine bill 4201&lt;/em&gt; and ask about Kyiv next July.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://clockwrit.vercel.app/desk" rel="noopener noreferrer"&gt;Ruling desk&lt;/a&gt;&lt;/strong&gt; — draft and approve a ruling. &lt;strong&gt;Reviewer passcode: &lt;code&gt;every-hour-has-a-citation&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://clockwrit.vercel.app/audit?demo=1" rel="noopener noreferrer"&gt;Audit&lt;/a&gt;&lt;/strong&gt; — a weekly meeting across five cities, every occurrence checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://clockwrit.vercel.app/eval" rel="noopener noreferrer"&gt;Eval&lt;/a&gt;&lt;/strong&gt; — every question, every answer, every verdict.&lt;/li&gt;
&lt;/ol&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%2Fwtdo7s4sl5spb80yoozs.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%2Fwtdo7s4sl5spb80yoozs.png" alt="Four clocks, one instant" width="800" height="549"&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/RaYYeR220" rel="noopener noreferrer"&gt;
        RaYYeR220
      &lt;/a&gt; / &lt;a href="https://github.com/RaYYeR220/clockwrit" rel="noopener noreferrer"&gt;
        clockwrit
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      What time is it, legally? An agent on Sanity Context that reads the law, IANA tzdata and your runtime's clock, and tells you which one is right.
    &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;Clockwrit&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;What time is it — legally?&lt;/strong&gt; An agent that reads the law, IANA tzdata and your runtime’s clock, and tells you which one is right, with the decree, the date and the source quote. Built on &lt;a href="https://www.sanity.io/context" rel="nofollow noopener noreferrer"&gt;Sanity Context&lt;/a&gt;: a typed dataset for the facts, a Knowledge Base for what the sources say, and a ruling loop for when they disagree.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://clockwrit.vercel.app" rel="nofollow noopener noreferrer"&gt;https://clockwrit.vercel.app&lt;/a&gt; · &lt;strong&gt;Video (3 min):&lt;/strong&gt; &lt;a href="https://youtu.be/pSU875tMuzw" rel="nofollow noopener noreferrer"&gt;https://youtu.be/pSU875tMuzw&lt;/a&gt; · &lt;strong&gt;Ask:&lt;/strong&gt; &lt;a href="https://clockwrit.vercel.app/ask" rel="nofollow noopener noreferrer"&gt;/ask&lt;/a&gt; · &lt;strong&gt;Ruling desk:&lt;/strong&gt; &lt;a href="https://clockwrit.vercel.app/desk" rel="nofollow noopener noreferrer"&gt;/desk&lt;/a&gt; · &lt;strong&gt;Eval:&lt;/strong&gt; &lt;a href="https://clockwrit.vercel.app/eval" rel="nofollow noopener noreferrer"&gt;/eval&lt;/a&gt; · &lt;strong&gt;MCP:&lt;/strong&gt; &lt;code&gt;https://clockwrit.vercel.app/api/mcp&lt;/code&gt; · Reviewing? Start with &lt;a href="https://github.com/RaYYeR220/clockwrit/JUDGES.md" rel="noopener noreferrer"&gt;JUDGES.md&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/RaYYeR220/clockwrit/docs/architecture.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FRaYYeR220%2Fclockwrit%2FHEAD%2Fdocs%2Farchitecture.png" alt="Architecture"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why this exists&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;In 2026 Canada stopped changing its clocks one province at a time: British Columbia, Alberta, the Northwest Territories, then Manitoba from 31 October. IANA shipped five tzdata releases (2026a–e) to keep up, and for some of them deliberately models the change on 1 November at 02:00 — a documented “temporary hack” — while the law takes effect on another…&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/RaYYeR220/clockwrit" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;A pnpm monorepo: a deterministic legal-time engine (&lt;code&gt;packages/core&lt;/code&gt;, 30 tests), the Next.js app and agent (&lt;code&gt;web&lt;/code&gt;), the Studio and schema (&lt;code&gt;studio&lt;/code&gt;), the App SDK Dashboard app (&lt;code&gt;desk&lt;/code&gt;), the dataset and the 90-source corpus (&lt;code&gt;knowledge&lt;/code&gt;), the evaluation (&lt;code&gt;eval&lt;/code&gt;) and the CLI (&lt;code&gt;packages/cli&lt;/code&gt;). MIT.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&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%2Fw7sbtfj7yaojetsvj35q.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%2Fw7sbtfj7yaojetsvj35q.png" alt="Architecture" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A typed dataset where every fact points at its law
&lt;/h3&gt;

&lt;p&gt;The schema is the product. A &lt;code&gt;ruleSegment&lt;/code&gt; is one zone's clock regime over one span of time — the standard offset, the DST rule written the way the law (and zic) writes it (&lt;code&gt;Sun&amp;gt;=8 2:00 wall&lt;/code&gt;), &lt;code&gt;validFrom&lt;/code&gt;/&lt;code&gt;validTo&lt;/code&gt; as UTC instants — and a required &lt;code&gt;basis[]&lt;/code&gt; that references the &lt;strong&gt;instrument&lt;/strong&gt; it rests on. An &lt;code&gt;instrument&lt;/code&gt; is anything that makes a claim: a statute, a decree, a bill, an IANA release, a Microsoft notice, a news article, a holiday library. Each has an &lt;strong&gt;authority tier&lt;/strong&gt; (primary / secondary / community), a &lt;strong&gt;legal status&lt;/strong&gt; (in force, enacted-not-effective, conditional, pending bill, proposed, vetoed…), dates, and verbatim quotes in their original language with translations.&lt;/p&gt;

&lt;p&gt;The calendar side has &lt;code&gt;holiday&lt;/code&gt; (with &lt;code&gt;dateCertainty&lt;/code&gt;: fixed by law, announced, or merely calculated — Eid moves on a moon sighting), &lt;code&gt;weekendRegime&lt;/code&gt; (the UAE moved its weekend in 2022), &lt;code&gt;workdayOverride&lt;/code&gt; (China's adjusted working Saturdays) and &lt;code&gt;holidaySuspension&lt;/code&gt; (Ukraine's martial law suspends holiday days off). Each &lt;code&gt;jurisdiction&lt;/code&gt; records which years have a &lt;em&gt;complete&lt;/em&gt; holiday list, so the agent knows when it's allowed to say "working day".&lt;/p&gt;

&lt;p&gt;845 documents, 179 instruments (83 of them primary law), 47 zones, 58 jurisdictions. Every one of the 114 clock regimes is checked against IANA tzdata 2026e — daily and hourly around every transition, 2020–2028 — before it's loaded.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Content Releases as pending legislation
&lt;/h3&gt;

&lt;p&gt;A release is a set of document versions that publish together. That's exactly what a bill is. Seven releases hold the clock regimes as they would read if each law took effect, and the agent's tools read the dataset with &lt;code&gt;perspective: [releaseId]&lt;/code&gt;. Asking "what if Ukraine's bill is signed?" isn't a prompt trick — it's the same deterministic computation over a different perspective.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Two Sanity Context MCP endpoints
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;clockwrit-catalog&lt;/code&gt;&lt;/strong&gt; — GROQ mode over the dataset. The agent uses &lt;code&gt;groq_query&lt;/code&gt; and &lt;code&gt;schema_explorer&lt;/code&gt; to find instruments, statuses and dates. The endpoint's instructions explain what authority and status mean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;clockwrit-sources&lt;/code&gt;&lt;/strong&gt; — Knowledge Base mode. The agent uses &lt;code&gt;knowledge_base_search&lt;/code&gt; / &lt;code&gt;knowledge_base_read&lt;/code&gt; for what the sources actually say.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both endpoints' &lt;code&gt;/initial-context&lt;/code&gt; is inlined into the system prompt so the agent doesn't spend a tool call on orientation. Every Ask answer shows an &lt;strong&gt;evidence trail&lt;/strong&gt; of the Context tool calls it made. Conversations are saved to Context Insights through &lt;code&gt;@sanity/context&lt;/code&gt;'s AI SDK integration.&lt;/p&gt;

&lt;p&gt;The agent never states an offset or a working day from memory. Three deterministic tools — &lt;code&gt;legal_time&lt;/code&gt;, &lt;code&gt;working_day&lt;/code&gt;, &lt;code&gt;audit_schedule&lt;/code&gt; — read the typed records and compute. &lt;strong&gt;Code decides; the model explains.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A Knowledge Base built from 90 real sources — in two passes
&lt;/h3&gt;

&lt;p&gt;I imported the corpus in two phases, each source with a header naming its authority tier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase A, the law first:&lt;/strong&gt; statutes, proclamations, gazettes, government announcements, IANA NEWS entries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase B, then the internet:&lt;/strong&gt; the news headline that overstated a bill, the Microsoft notice with a different date, the Wikipedia line its own citation doesn't support, the holiday library that's a day off for Pakistan's Eid.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. The ruling loop: Knowledge Base issues → instructions
&lt;/h3&gt;

&lt;p&gt;When the build finds a fact the sources disagree on, it files a &lt;strong&gt;conflict issue&lt;/strong&gt; with both sides and the exact quoted lines. The one that matters most: &lt;em&gt;Manitoba's proclamation says 31 October; IANA 2026e says 1 November 02:00.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the ruling desk the agent drafts a ruling — which side, why (authority, status, dates), and a cross-check against the typed records — through a forced tool call. &lt;strong&gt;It cannot apply it.&lt;/strong&gt; Applying is a code path behind a gate: the reviewer passcode, or — from the Dashboard app — the reviewer's own Sanity token, accepted only if a dry-run edit of that ruling succeeds with their permissions &lt;em&gt;and&lt;/em&gt; they administer the organisation that owns the Knowledge Base. Approval resolves the issue through the Knowledge Base API, which mints an &lt;strong&gt;instruction&lt;/strong&gt; that every later build follows, and writes the ruling back to the dataset.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. The App SDK desk
&lt;/h3&gt;

&lt;p&gt;A Dashboard app lists rulings live (&lt;code&gt;useDocuments&lt;/code&gt;, &lt;code&gt;useDocumentProjection&lt;/code&gt;), lets an editor approve with their own identity (&lt;code&gt;useAuthToken&lt;/code&gt;), and shows the pending laws (&lt;code&gt;useActiveReleases&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this only works because the content is structured
&lt;/h3&gt;

&lt;p&gt;I wrote 40 questions from the verified research &lt;em&gt;before&lt;/em&gt; the agent existed, each with an answer key and a trap reason. Ten were used while building; the 30 held-out ones were run once against three arms, graded by a model from a different family:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;th&gt;Traps correct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clockwrit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30 / 30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14 / 14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same model, no tools&lt;/td&gt;
&lt;td&gt;22 / 30&lt;/td&gt;
&lt;td&gt;8 / 14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyword search (BM25) over the same 90 sources + same model&lt;/td&gt;
&lt;td&gt;16 / 30&lt;/td&gt;
&lt;td&gt;6 / 14&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model alone is confidently wrong exactly where the world changed this year: it says Casablanca is UTC+1 (it isn't, since 20 September), that 01:30 on 20 September happened once there (twice), that Pakistan's Eid al-Adha was the 28th. &lt;strong&gt;Keyword search made it worse than no retrieval at all&lt;/strong&gt; — 8 abstentions — because the passages that match the words rarely carry the effective date, the legal status or the instant that decides the answer. Every answer and verdict is on &lt;a href="https://clockwrit.vercel.app/eval" rel="noopener noreferrer"&gt;the eval page&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%2F5ja05b0ktrh4tusm367v.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%2F5ja05b0ktrh4tusm367v.png" alt="Eval" width="800" height="944"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What surprised me (and what I'm not claiming)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Knowledge Base is smarter than I planned for.&lt;/strong&gt; My first test fed it a news headline ("Rada abolished the clock change") and the bill card ("never signed"). It didn't raise a conflict — it wrote "adopted but not in force", which is &lt;em&gt;correct&lt;/em&gt;. Conflicts only appear when claims truly can't both be true. That's why the second import pass matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't rate authority on its own.&lt;/strong&gt; Conflict sides came back with no authority tier, and in one test it rated a community source as primary. Clockwrit takes authority from the typed records instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A full rebuild restructures every entry.&lt;/strong&gt; Decisions survive (issues keep a stable fingerprint, instructions persist) — the entry paths don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The legal date and tzdata disagree on purpose.&lt;/strong&gt; For Manitoba and the NWT the dataset records the legal effective instant; the UTC offset is identical either way, only the DST flag and abbreviation differ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not claimed:&lt;/strong&gt; global coverage (47 zones and 58 jurisdictions where something changed or is contested; elsewhere it says so), discovery of unknown facts (the eval measures whether structure gets the agent to the right answer), legal advice. The full ledger is &lt;a href="https://github.com/RaYYeR220/clockwrit/blob/main/docs/CLAIMS.md" rel="noopener noreferrer"&gt;docs/CLAIMS.md&lt;/a&gt;.&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;c9x90tjo&lt;/code&gt; — dataset &lt;code&gt;production&lt;/code&gt; (public)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public dataset query:&lt;/strong&gt; &lt;a href="https://c9x90tjo.api.sanity.io/v2026-09-01/data/query/production?query=*%5B_type%3D%3D%22instrument%22%5D%7Btitle%2Cauthority%2Cstatus%2Curl%7D" rel="noopener noreferrer"&gt;every instrument with its authority and status&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Studio:&lt;/strong&gt; &lt;a href="https://clockwrit.sanity.studio" rel="noopener noreferrer"&gt;https://clockwrit.sanity.studio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context MCP endpoints:&lt;/strong&gt; &lt;code&gt;clockwrit-catalog&lt;/code&gt; (dataset, GROQ mode), &lt;code&gt;clockwrit-sources&lt;/code&gt; (Knowledge Base mode)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use it from your own agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http clockwrit https://clockwrit.vercel.app/api/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;No session transcript is attached to this entry.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built an agent that reads 200-year-old handwriting — the interesting part is what it refuses to do</title>
      <dc:creator>Kateryna Ivashchenko</dc:creator>
      <pubDate>Mon, 31 Aug 2026 23:48:38 +0000</pubDate>
      <link>https://dev.to/rayyer/i-built-an-agent-that-reads-200-year-old-handwriting-the-interesting-part-is-what-it-refuses-to-do-2i30</link>
      <guid>https://dev.to/rayyer/i-built-an-agent-that-reads-200-year-old-handwriting-the-interesting-part-is-what-it-refuses-to-do-2i30</guid>
      <description>&lt;p&gt;The National Archives Catalog holds 34,309,409 records. Most of the handwritten material in it has never been transcribed, which means it is not full-text searchable, which means it is effectively invisible: you can only find a document if you already know it exists. The people who fix that are volunteers in the Citizen Archivist programme, typing one page at a time.&lt;/p&gt;

&lt;p&gt;I spent a build cycle on a partner for that volunteer. Along the way I hit four things that cost me real time and that I have not seen written down anywhere, so here they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The catalog answers HTTP 200 with an HTML page when you are wrong
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;catalog.archives.gov&lt;/code&gt; exposes a full JSON API behind &lt;code&gt;/proxy/*&lt;/code&gt;. It is unauthenticated, it works from outside the US, and it is genuinely good. But it has a failure mode that will eat an afternoon: &lt;strong&gt;when your request is invalid, or when you have been going too fast, it returns &lt;code&gt;200 OK&lt;/code&gt; with the single-page app's HTML shell.&lt;/strong&gt; Not a 400. Not a 429. A 200, with a &lt;code&gt;text/html&lt;/code&gt; body, 5,454 bytes every time.&lt;/p&gt;

&lt;p&gt;So the only reliable success signal is the content type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ShellResponse&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;   &lt;span class="c1"&gt;# retryable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worse, &lt;code&gt;limit&lt;/code&gt; is not a range — it is an &lt;strong&gt;allowlist&lt;/strong&gt;: &lt;code&gt;{1, 10, 20, 50, 75, 100, 1000, 10000}&lt;/code&gt;. &lt;code&gt;limit=3&lt;/code&gt; and &lt;code&gt;limit=5&lt;/code&gt; both silently return that same shell. I lost about an hour to &lt;code&gt;limit=5&lt;/code&gt; before I checked the content type instead of the status code.&lt;/p&gt;

&lt;p&gt;Two more from the same afternoon: pagination is &lt;code&gt;page&lt;/code&gt;, not &lt;code&gt;offset&lt;/code&gt;; and &lt;code&gt;transcriptions_exist=true&lt;/code&gt; is accepted and then &lt;strong&gt;ignored&lt;/strong&gt; — it returns identical counts for &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt; and a nonsense value. Scope by series with &lt;code&gt;ancestorNaId&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Gemini free tier is 20 requests per &lt;em&gt;day&lt;/em&gt;, and retrying makes it worse
&lt;/h2&gt;

&lt;p&gt;I assumed a 429 meant "slow down". It can also mean "come back tomorrow":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GenerateRequestsPerDayPerProjectPerModel-FreeTier   value: 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty requests per day, per model, per project. My evaluation harness had a perfectly sensible retry policy — six attempts, exponential backoff — and it turned a spent budget into a &lt;em&gt;very&lt;/em&gt; spent budget, because every retry against a daily cap is another request from a bucket that is already empty.&lt;/p&gt;

&lt;p&gt;The fix is to tell the two apart before deciding to retry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_DAILY_MARKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;perday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_day&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requestsperday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;free_tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;freetier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_is_daily_quota&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;429&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_DAILY_MARKERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A per-minute limit clears if you wait. A per-day limit does not, and retrying it is pure waste.&lt;/p&gt;

&lt;p&gt;The way out, incidentally, is not a bigger free tier — it is &lt;strong&gt;Vertex AI&lt;/strong&gt;, which bills through Cloud and has no daily cap of this kind. Which leads to:&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Vertex and AI Studio are not the same API, and one model lives on only one of them
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;google-genai&lt;/code&gt; gives you one &lt;code&gt;Client&lt;/code&gt; for both, which makes it easy to assume they are interchangeable. They are not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interactions API&lt;/strong&gt; (&lt;code&gt;client.interactions.create&lt;/code&gt;) is the current AI Studio surface and it supersedes &lt;code&gt;generate_content&lt;/code&gt;. On Vertex it answers &lt;code&gt;400 Unsupported model interaction: gemini-3.7-flash&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;So on Vertex you use &lt;code&gt;generate_content&lt;/code&gt; with &lt;code&gt;response_mime_type&lt;/code&gt; + &lt;code&gt;response_schema&lt;/code&gt; — the pair that is marked &lt;em&gt;deprecated&lt;/em&gt; on the AI Studio path in favour of &lt;code&gt;response_format&lt;/code&gt;. Both are correct; which one is correct depends on the endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma is not a Vertex publisher model.&lt;/strong&gt; &lt;code&gt;gemma-4-31b-it&lt;/code&gt; is a plain 404 there — on Vertex it only exists behind a Model Garden deployment you provision and pay for. The AI Studio endpoint serves it directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I run Gemini on Vertex and Gemma on AI Studio in the same process, which needs one more thing that is easy to miss: &lt;code&gt;GOOGLE_GENAI_USE_VERTEXAI&lt;/code&gt; is read process-wide, so a client you construct with &lt;code&gt;api_key=&lt;/code&gt; still routes to Vertex unless you say &lt;code&gt;vertexai=False&lt;/code&gt; explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;client_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemma&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;on_vertex&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vertexai&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# explicit, or the env var wins
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;get_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. ADK 2.8: the workflow agents are deprecated, and tool confirmation does not work in a graph
&lt;/h2&gt;

&lt;p&gt;Two things changed under the tutorials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SequentialAgent&lt;/code&gt;, &lt;code&gt;ParallelAgent&lt;/code&gt; and &lt;code&gt;LoopAgent&lt;/code&gt; are all &lt;code&gt;@deprecated&lt;/code&gt; in 2.8&lt;/strong&gt;, superseded by &lt;code&gt;google.adk.workflow.Workflow&lt;/code&gt; — a real graph runtime. &lt;code&gt;BaseAgent&lt;/code&gt; now subclasses &lt;code&gt;workflow.BaseNode&lt;/code&gt;, so agents &lt;em&gt;are&lt;/em&gt; nodes and inherit &lt;code&gt;retry_config&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;rerun_on_resume&lt;/code&gt; and &lt;code&gt;state_schema&lt;/code&gt;. Retries and timeouts stop being wrapper code and become properties of the topology, which is a much nicer place for them.&lt;/p&gt;

&lt;p&gt;Two runtime behaviours you only find by running it: &lt;code&gt;ctx.run_node()&lt;/code&gt; raises unless the &lt;strong&gt;calling&lt;/strong&gt; node has &lt;code&gt;rerun_on_resume=True&lt;/code&gt;, and a &lt;code&gt;JoinNode&lt;/code&gt; placed behind a &lt;em&gt;conditional&lt;/em&gt; fan-out never completes, because it waits for all predecessors and one of them never runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the one that actually cost me a design:&lt;/strong&gt; &lt;code&gt;FunctionTool(fn, require_confirmation=True)&lt;/code&gt; — the human-in-the-loop gate — is implemented in the &lt;code&gt;LlmAgent&lt;/code&gt; flow, in &lt;code&gt;flows/llm_flows/request_confirmation.py&lt;/code&gt;. A &lt;code&gt;Workflow&lt;/code&gt;'s tool node builds a fresh &lt;code&gt;ToolContext&lt;/code&gt;, calls &lt;code&gt;tool.run_async&lt;/code&gt; and yields the result. It &lt;strong&gt;never emits &lt;code&gt;adk_request_confirmation&lt;/code&gt;&lt;/strong&gt; and never resumes. Drop a confirmation-gated tool into a graph and it will run straight through, silently, exactly as if you had not asked for a gate.&lt;/p&gt;

&lt;p&gt;What works: raise a workflow-native &lt;code&gt;RequestInput&lt;/code&gt; carrying the pending call, and on resume re-validate the arguments against a digest you recorded when approval was requested — &lt;em&gt;then&lt;/em&gt; hand it to the confirmation-gated tool. You keep the property that matters, which is that an approver cannot alter the call they approved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I did not want to write
&lt;/h2&gt;

&lt;p&gt;The whole product is built on one idea: the agent should learn the volunteer's editorial conventions from their corrections, so the hundredth page costs less attention than the first.&lt;/p&gt;

&lt;p&gt;So I tested it properly. Mine conventions from corrections on 60 training pages, apply them to 60 pages the miner never saw, grade the same model reading twice — once raw, once styled — so the comparison is paired and sampling noise cannot move it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1 page improved. 57 unchanged. 2 worsened. Mean CER change: −0.0001.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Six conventions were mined, each at 100% precision on training data. They simply do not fire often enough on unseen pages to move a median. My best guess is the shape of the material: abbreviations in Revolutionary War pension files are long-tailed, so a clerk's contraction learned from one veteran's file rarely shows up in another. The experiment that would settle it learns &lt;em&gt;within a single file unit&lt;/em&gt;, where one hand repeats across dozens of leaves. I have not run it.&lt;/p&gt;

&lt;p&gt;An earlier version was worse. I had seeded plausible conventions by hand — &lt;code&gt;Recd&lt;/code&gt; → &lt;code&gt;Received&lt;/code&gt; — and it scored &lt;strong&gt;worse than doing nothing&lt;/strong&gt;, because archival practice is to transcribe verbatim. Expanding the abbreviation moves the text away from what the volunteer actually typed. Guessing at someone's editorial style is exactly the mistake the product exists to prevent, which is a funny way to learn a lesson.&lt;/p&gt;

&lt;p&gt;The transcription itself is fine — median CER 6.8% against transcriptions written by people, best page 0.29%. It is the learning claim that is unproven, and a result that only reports its wins is not a result.&lt;/p&gt;




&lt;p&gt;Code: &lt;a href="https://github.com/RaYYeR220/longhand" rel="noopener noreferrer"&gt;https://github.com/RaYYeR220/longhand&lt;/a&gt; · Live: &lt;a href="https://longhand-494617981995.us-central1.run.app" rel="noopener noreferrer"&gt;https://longhand-494617981995.us-central1.run.app&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
