<?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: Faris </title>
    <description>The latest articles on DEV Community by Faris  (@faris-khan).</description>
    <link>https://dev.to/faris-khan</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%2F4112006%2Fc337d045-2c5b-424a-8012-aa27f22ea1eb.png</url>
      <title>DEV Community: Faris </title>
      <link>https://dev.to/faris-khan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/faris-khan"/>
    <language>en</language>
    <item>
      <title>Everyone Wanted to Help. Nobody Had the Whole Plan</title>
      <dc:creator>Faris </dc:creator>
      <pubDate>Sat, 24 Oct 2026 19:22:07 +0000</pubDate>
      <link>https://dev.to/faris-khan/everyone-wanted-to-help-nobody-had-the-whole-plan-2pjk</link>
      <guid>https://dev.to/faris-khan/everyone-wanted-to-help-nobody-had-the-whole-plan-2pjk</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Alex sees the appointment change. Maya sees the request for a ride. Sam assumes someone else has the groceries covered. Robin checks the chat after work and scrolls through yesterday's messages, trying to find which plan is still current.&lt;/p&gt;

&lt;p&gt;These are fictional people in CareRelay's synthetic demonstration. The burden they represent is familiar: the work of helping includes a second, repetitive job of remembering who said what, checking what changed, and finding out whether someone actually agreed to help.&lt;/p&gt;

&lt;p&gt;CareRelay is a local family coordination prototype. It turns pasted messages into suggested appointments, transport, errands, pickups and confirmation tasks. A person reviews the evidence and approves each item before it reaches the shared plan. A caregiver then accepts their own responsibilities. Those are two different decisions, and the application keeps them separate.&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%2F9dkvuhti6v5rhi4r1z8d.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%2F9dkvuhti6v5rhi4r1z8d.png" alt="CareRelay overview showing the fictional circle and its approved coordination plan" width="800" height="450"&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/ylEdw_ICXTU" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/ylEdw_ICXTU" rel="noopener noreferrer"&gt;Watch the complete CareRelay demonstration on YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The demonstration was recorded at 1920×1080 with real local model inference and synthetic family messages. The 108-second export uses Kokoro-82M narration with the &lt;code&gt;am_fenrir&lt;/code&gt; voice and an English subtitle track. Only the unused closing hold was trimmed; the visible actions and model-processing wait were preserved. The recording script, silent source, narration, captions and final video are included with the project.&lt;/p&gt;

&lt;p&gt;The recorded journey follows a clinic appointment change, a proposed driver whose work shift overlaps the journey, and an unassigned return ride. Robin explicitly accepts her own library pickup. The original appointment becomes cancelled when its replacement is approved. The final handoff keeps that accepted commitment separate from the unresolved replacement appointment and its transport question.&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%2Fnkhzmp03e2p8idvcjd40.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%2Fnkhzmp03e2p8idvcjd40.png" alt="Synthetic conversation in the import inbox, with an explicit reference time and visibility choice" width="800" height="450"&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%2F3zk0gkdtwyebur8p025r.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%2F3zk0gkdtwyebur8p025r.png" alt="Human-approved handoff separating accepted commitments from unresolved responsibilities" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/syncaimain/CareRelay" rel="noopener noreferrer"&gt;CareRelay source code, setup instructions and demonstration assets&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The MIT-licensed application uses Next.js, TypeScript, Tailwind, a shadcn-style button component, FastAPI, Pydantic and SQLite. The repository includes the labeled synthetic dataset, evaluation reports, recording script, screenshots and narrated video. Model weights retain their upstream licenses. The README documents manual local startup; the optional Docker Compose configuration has not been verified.&lt;/p&gt;

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

&lt;p&gt;I used Ministral 3 14B through Ollama, an Apache 2.0 open-weight model. The core inference operation produces structured coordination suggestions from actual source text. There is no fake-response fallback in the application.&lt;/p&gt;

&lt;p&gt;A typed JSON schema constrains the response, but valid JSON alone is insufficient evidence. The backend checks that every quote is an exact contiguous substring of the imported source and that the source ID matches. A failed evidence check rejects the result before suggestions are written. Interpreted dates carry an explicit review warning. A name in a model result does not constitute consent.&lt;/p&gt;

&lt;p&gt;The review queue is deliberately central. The reviewer can correct the title, date, location, proposed owner and unresolved details. Approval places an item on the plan. Acceptance is a separate action reserved for the proposed caregiver, or a volunteer when the task is unassigned.&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%2Fli6sm0x6wpz74ofi4h23.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%2Fli6sm0x6wpz74ofi4h23.png" alt="Review queue showing model suggestions beside source evidence and human review controls" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That distinction changes the familiar task-board interaction. A task can be perfectly understandable and approved while still having nobody committed to doing it. CareRelay displays that gap instead of filling it with a reassuring name. Unclaimed transport gets an explicit warning; the calendar is a view of the approved plan rather than a second independent source of truth.&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%2F70uftkn39hryqunftjzj.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%2F70uftkn39hryqunftjzj.png" alt="Responsibility board keeping proposed assignments and unclaimed transport visible" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Availability is intentionally modest. Caregivers record windows when they are unavailable. The conflict check compares proposed tasks against those windows and against each other, using a documented one-hour planning window. It does not pretend to know travel times, appointment duration or whether two nearby errands are realistically compatible. A warning is a reason to check with someone, not a scheduling verdict.&lt;/p&gt;

&lt;p&gt;Cancellation and rescheduling retain the original record. A reviewer chooses the original approved item, which is cancelled when its replacement is approved. Acceptance resets for the replacement. Transport remains a separate responsibility; changing an appointment never silently means a driver has accepted a different journey.&lt;/p&gt;

&lt;p&gt;Handoffs take a conservative approach to factuality. The local model returns references to supplied approved tasks. The backend validates those references and renders the brief deterministically from approved records, including all relevant tasks to prevent omissions. Private sources never enter a shareable brief. Draft approval records task versions; if the plan changes, the user must generate a fresh handoff.&lt;/p&gt;

&lt;p&gt;I deliberately avoided asking the model to turn the board into unrestricted prose. A beautifully written handoff that quietly drops an unassigned ride is worse than a plain list that keeps it visible. The model can help select references, but it cannot introduce new facts into the renderer or erase approved tasks by leaving them out. The resulting message is less literary and easier to audit.&lt;/p&gt;

&lt;p&gt;The backend also draws a boundary around each imported source. A source is shared with the circle or private to its importer. A coordinator is not automatically allowed to read another person's private material. Import, review and briefing permissions are checked on the server, and responsibility acceptance is restricted to the person concerned. The activity view shows the signed-in person's own events. None of these decisions depend on a hidden button in the browser.&lt;/p&gt;

&lt;p&gt;The implementation stays small enough to run locally: parameterized SQLite queries and transactions, explicit state transitions, Pydantic request validation, and an in-process inference job. An identical source with the same reference timestamp is not repeatedly imported. If inference fails, the job remains visibly failed and can be retried. A server restart marks unfinished jobs for retry instead of presenting them as completed work.&lt;/p&gt;

&lt;p&gt;The current prototype is intended for fictional data on a trusted local machine. It does not yet provide production identity verification or encrypted storage. The demo's profile switcher exists to demonstrate consent and permission boundaries, not to claim independent authentication for real households.&lt;/p&gt;

&lt;p&gt;The interface was revised after visual review: white and slate surfaces, teal accents, sans-serif headings, larger controls, and clearer secondary text. All 23 deterministic backend/evaluation tests, TypeScript checking, a full production build and a genuine Chromium journey pass. Desktop and mobile screenshots were inspected at 1440×1000 and 390×844, with no detected horizontal mobile overflow or browser page errors.&lt;/p&gt;

&lt;p&gt;Measurement also changed the implementation. The initial 32-message synthetic evaluation scored 77.8% strict category precision and recall. Confirmations were sometimes classified as appointments; cancellations sometimes changed the task category; the model sometimes assumed UTC even though the source reference carried a local timezone.&lt;/p&gt;

&lt;p&gt;The extraction instructions now define categories by the requested action and explicitly supply the reference timezone. A later development run measured 92.9% precision and 96.3% recall, with 26 true positives, two false positives and one false negative. Owner attribution was 11/12, explicit timestamps 21/22 and change operations 3/3. The complete outputs and code/dataset hashes are preserved in the project.&lt;/p&gt;

&lt;p&gt;Those are development-set measurements, not held-out accuracy: the prompt was refined after inspecting the same corpus. One remaining disagreement concerns whether collecting groceries should be a pickup or an errand; the original reference label was retained. A separate extra suggestion arose from a request to check availability. Missing or multiple predictions count against the applicable field metrics rather than disappearing from the denominator.&lt;/p&gt;

&lt;p&gt;The recorded reschedule also produced an unsupported ride cancellation suggestion. The quote was genuine, but the source had requested a separate transport check, not cancellation. That suggestion was rejected. This is why source provenance and semantic correctness are different checks, and why approval remains a real decision rather than a decorative button.&lt;/p&gt;

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

&lt;p&gt;Local weights let this coordination workflow run without sending family messages to a paid inference service. The model can be replaced and the extraction measured against the included labeled synthetic corpus. The schemas, validators, prompts and decision boundaries are visible in the source rather than hidden behind a claim that the model will simply get the details right.&lt;/p&gt;

&lt;p&gt;The useful question is not whether an AI can produce a plausible plan. It is whether the people doing the work can see why an item exists, correct it, and decide whether they are willing to own it.&lt;/p&gt;

&lt;p&gt;There is also value in being able to keep the successful and unsuccessful runs side by side. The initial model report has not been replaced with a polished headline number. Its mistakes explain the stronger timezone instruction, category editing, and the continued need for review. The prompts, raw structured outputs, validators and measured results travel with the source, so another builder can reproduce the exercise rather than accepting my preferred example.&lt;/p&gt;

&lt;p&gt;Open weights do not make the application trustworthy by themselves. They make inspection and replacement possible. The trust comes from the smaller surrounding decisions: preserve the quote, expose uncertainty, distinguish approval from consent, and refuse to call an unfinished responsibility resolved.&lt;/p&gt;

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

&lt;p&gt;The intended payoff is fewer rounds of reconstructing the same conversation. Imported messages become a single review queue, approved responsibilities appear together, and the next caregiver can start from a brief that distinguishes commitments from unresolved details.&lt;/p&gt;

&lt;p&gt;No household time savings have been measured. The included examples are synthetic demonstrations, not live-family trials or evidence of improved health outcomes.&lt;/p&gt;

&lt;p&gt;Alex still has to ask whether Maya can drive. Robin still has to decide whether she can help. CareRelay leaves those decisions with them. The smaller relief is having one place to see which questions still need an answer, without asking everyone to reconstruct the conversation again.&lt;/p&gt;

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

&lt;p&gt;I am entering the overall Week 3 challenge. No partner prize category is claimed because no qualifying partner integration is implemented.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Reading or Guessing? An Urdu Font-Twin Benchmark</title>
      <dc:creator>Faris </dc:creator>
      <pubDate>Sun, 11 Oct 2026 12:29:51 +0000</pubDate>
      <link>https://dev.to/faris-khan/reading-or-guessing-an-urdu-font-twin-benchmark-1lek</link>
      <guid>https://dev.to/faris-khan/reading-or-guessing-an-urdu-font-twin-benchmark-1lek</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;p&gt;Urdu OCR has an unusually revealing stress test built into its typography. Nastaliq is diagonal, context-heavy, and visually dense; Naskh is more linear. If a multimodal model is truly reading the glyphs, changing only the font should not change its transcription very much. If it is filling gaps with language priors, the difference may grow when the text stops being predictable.&lt;/p&gt;

&lt;p&gt;That led to one deliberately narrow question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Will a model transcribe the exact same Urdu Unicode string differently when it is rendered in Nastaliq instead of Naskh?&lt;/p&gt;
&lt;/blockquote&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%2F7rlnoe144bmj9mwfd2bz.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%2F7rlnoe144bmj9mwfd2bz.png" alt="The same pseudo-Urdu source rendered in Nastaliq and Naskh" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I built a paired multimodal OCR benchmark with three language conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Natural:&lt;/strong&gt; revision-pinned Urdu Wikipedia sentences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shuffled:&lt;/strong&gt; the same words in a deterministic shuffled order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pseudo:&lt;/strong&gt; deterministic Urdu-like character sequences that preserve spaces, punctuation, and token lengths while removing ordinary word predictability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every item is rendered twice: once in &lt;strong&gt;Noto Nastaliq Urdu&lt;/strong&gt; and once in &lt;strong&gt;Noto Naskh Arabic&lt;/strong&gt;. The two images in a pair contain the exact same NFC-normalized Unicode string and share a source-text SHA-256. The prompt is also identical: transcribe only what is visible, with no source text or font hint supplied to the model.&lt;/p&gt;

&lt;p&gt;The pilot has &lt;strong&gt;15 source items x 3 conditions x 2 fonts = 90 images&lt;/strong&gt;, organized as 45 exact-text font pairs.&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%2Flnnslukilq02zkoin6wo.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%2Flnnslukilq02zkoin6wo.png" alt="Pilot design" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Rendering was part of the experiment
&lt;/h3&gt;

&lt;p&gt;This benchmark would be meaningless if one font silently fell back, clipped glyphs, or simply rendered smaller. Before making model calls, I therefore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shaped every string with HarfBuzz using explicit &lt;code&gt;rtl&lt;/code&gt;, &lt;code&gt;Arab&lt;/code&gt;, and &lt;code&gt;ur&lt;/code&gt; properties;&lt;/li&gt;
&lt;li&gt;rasterized those glyph IDs directly with FreeType, preventing operating-system font fallback;&lt;/li&gt;
&lt;li&gt;verified character coverage, GSUB/GPOS tables, ligatures, RTL cluster order, image bounds, and nonblank output;&lt;/li&gt;
&lt;li&gt;calibrated each font against visible text height instead of assigning the same nominal point size; and&lt;/li&gt;
&lt;li&gt;generated a contact sheet for human inspection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Offline validation passed all &lt;strong&gt;90 images and 45 pairs&lt;/strong&gt;, with no missing codepoints, fallback, Unicode mismatch, clipping, or blank render.&lt;/p&gt;

&lt;p&gt;The text is public and traceable. Each natural sentence records its Urdu Wikipedia page URL and revision ID under CC BY-SA 4.0. I did &lt;strong&gt;not&lt;/strong&gt; add examples from the Urdu Newspaper Benchmark because its linked public data did not expose a dataset license that I could verify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scoring policy
&lt;/h3&gt;

&lt;p&gt;The primary metric is paired &lt;strong&gt;Nastaliq CER minus Naskh CER&lt;/strong&gt;. A positive gap means the model made more character errors on Nastaliq for the same source string. I also compute WER, insertions, deletions, substitutions, pseudo-word hallucinations into real words, paired bootstrap confidence intervals, and paired sign-flip tests.&lt;/p&gt;

&lt;p&gt;Scoring uses Unicode NFC and whitespace normalization. It does not merge distinct Urdu and Arabic letters such as U+06CC/U+064A, U+06A9/U+0643, or U+06C1/U+06BE.&lt;/p&gt;

&lt;p&gt;Two simple vision canaries, a large number and a short Urdu word, run before the benchmark. Endpoints that fail them are reported separately instead of being assigned an OCR score.&lt;/p&gt;

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

&lt;p&gt;I queried Kaggle's available 46-model catalog. The first expansion pass reached Kaggle's daily AI quota, so the current paired analysis includes the seven endpoints that completed all 90 image prompts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic Claude Haiku 4.5&lt;/li&gt;
&lt;li&gt;Anthropic Claude Haiku 5.5&lt;/li&gt;
&lt;li&gt;Anthropic Claude Sonnet 5&lt;/li&gt;
&lt;li&gt;Anthropic Claude Sonnet 5.5&lt;/li&gt;
&lt;li&gt;Google Gemini 3.5 Flash-Lite&lt;/li&gt;
&lt;li&gt;Google Gemini 3.7 Flash&lt;/li&gt;
&lt;li&gt;OpenAI GPT-5.4 Mini&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four of these also completed all 45 masked text-only prior controls. Partial image runs are retained for audit but excluded from the paired plots. Missing results are never imputed. The retry task uses a larger canary token allowance for reasoning models and will continue after quota refresh.&lt;/p&gt;

&lt;p&gt;That incompleteness is worth stating plainly: this is a &lt;strong&gt;validated pilot and an in-progress 46-model evaluation&lt;/strong&gt;, not a finished universal ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;p&gt;The clearest completed run was Gemini 3.7 Flash. Its mean Nastaliq-minus-Naskh CER gap was:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Paired CER gap&lt;/th&gt;
&lt;th&gt;95% bootstrap interval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Natural&lt;/td&gt;
&lt;td&gt;+0.18 points&lt;/td&gt;
&lt;td&gt;[0.00, 0.47] points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shuffled&lt;/td&gt;
&lt;td&gt;+0.25 points&lt;/td&gt;
&lt;td&gt;[0.00, 0.54] points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pseudo&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+8.75 points&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;[4.05, 12.82] points&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Its pseudo-word WER gap was &lt;strong&gt;+40.89 points&lt;/strong&gt;. Nastaliq pseudo-word outputs introduced nine natural-lexicon words absent from the references; Naskh introduced none. For this model, removing linguistic predictability exposed a much larger font gap than the natural and shuffled conditions.&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%2F1gt6nycdynbxlr7eee6g.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%2F1gt6nycdynbxlr7eee6g.png" alt="Character error rate heatmap" width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The multi-model picture is more interesting than a single clean headline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image-complete model&lt;/th&gt;
&lt;th&gt;Pseudo CER gap&lt;/th&gt;
&lt;th&gt;95% bootstrap interval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;+21.8 points&lt;/td&gt;
&lt;td&gt;[17.0, 26.6]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 5.5&lt;/td&gt;
&lt;td&gt;+4.9 points&lt;/td&gt;
&lt;td&gt;[-6.5, 15.6]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;
&lt;td&gt;+35.9 points&lt;/td&gt;
&lt;td&gt;[16.4, 57.0]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;-12.5 points&lt;/td&gt;
&lt;td&gt;[-33.5, 10.2]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.5 Flash-Lite&lt;/td&gt;
&lt;td&gt;+12.9 points&lt;/td&gt;
&lt;td&gt;[9.8, 16.3]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.7 Flash&lt;/td&gt;
&lt;td&gt;+8.8 points&lt;/td&gt;
&lt;td&gt;[4.1, 12.8]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.4 Mini&lt;/td&gt;
&lt;td&gt;+12.0 points&lt;/td&gt;
&lt;td&gt;[8.6, 14.9]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five of seven image-complete models had a positive pseudo-word interval excluding zero. Two did not support that direction, including one negative point estimate with a wide interval. Several Claude runs also showed substantial gaps on natural or shuffled text, which warns against reducing every font effect to language-prior reliance.&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%2Fdi901iws1u38hlsbft1h.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%2Fdi901iws1u38hlsbft1h.png" alt="Paired font comparison" width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I expected pseudo-words to be harder. I did not expect the &lt;strong&gt;difference between fonts&lt;/strong&gt; to expand so sharply for several models while remaining small or unstable for others. The font is not just cosmetic input formatting; for some multimodal systems it changes the effective task.&lt;/p&gt;

&lt;p&gt;The canaries were also informative. A model name appearing in a multimodal catalog does not guarantee that an endpoint will accept images, allocate enough visible output tokens, or follow an exact-transcription instruction. Separating endpoint failures from OCR errors prevented infrastructure behavior from becoming a fake accuracy score.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I would measure next
&lt;/h3&gt;

&lt;p&gt;The next run will complete the retry-eligible endpoints, add 0.5x and 0.25x resolution strata, and increase the source set from 15 to 60 items. I also want to add Tesseract Urdu and a public UrduOCR checkpoint, but only when the executable/model artifact and its reuse license are verifiably available. The core analysis will remain paired: the question is not merely which model has lower CER, but whether the &lt;em&gt;same model reading the same string&lt;/em&gt; changes when only its Urdu font changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video Walkthrough
&lt;/h2&gt;

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

&lt;p&gt;&lt;a href="https://youtu.be/wQcnfXLtA-E" rel="noopener noreferrer"&gt;Watch the walkthrough on YouTube&lt;/a&gt;, or &lt;a href="https://github.com/syncaimain/urdu-font-twin-benchmark/raw/main/media/urdu-font-twin-walkthrough.mp4" rel="noopener noreferrer"&gt;download the two-minute MP4 from GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;The Kaggle task, code, reproducible assets, font-validation report, and current run history are here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kaggle Benchmark:&lt;/strong&gt; &lt;a href="https://www.kaggle.com/benchmarks/tasks/huzaifa12344/reading-or-guessing-urdu-font-twins/9" rel="noopener noreferrer"&gt;Reading or Guessing? Urdu Font Twins, version 9&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source and reproducibility bundle:&lt;/strong&gt; &lt;a href="https://github.com/syncaimain/urdu-font-twin-benchmark" rel="noopener noreferrer"&gt;syncaimain/urdu-font-twin-benchmark&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository includes the deterministic renderer, source manifest, font licenses, canaries, metric implementation, analysis scripts, tests, contact sheet, and machine-readable status ledger. No benchmark score in this post is estimated or filled in from missing data.&lt;/p&gt;

&lt;p&gt;The experiment started with a typography question, but it ended up probing something broader: when a vision-language model produces the right word, how much of that answer came from seeing the letters, and how much came from knowing what word ought to be there?&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>I hard-killed my hiking safety app mid-trip. The alert still went out.</title>
      <dc:creator>Faris </dc:creator>
      <pubDate>Fri, 09 Oct 2026 19:57:26 +0000</pubDate>
      <link>https://dev.to/faris-khan/i-hard-killed-my-hiking-safety-app-mid-trip-the-alert-still-went-out-49a</link>
      <guid>https://dev.to/faris-khan/i-hard-killed-my-hiking-safety-app-mid-trip-the-alert-still-went-out-49a</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;The oldest piece of outdoor safety advice is also the most ignored: &lt;strong&gt;tell someone where you're going and when you'll be back.&lt;/strong&gt; The problem is that "someone" has to remember, has to notice you're late, and has to know what to do about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TrailWatch&lt;/strong&gt; is that someone. It's a hiking check-in timer that can't forget about you.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before you leave, you type one sentence: &lt;em&gt;"Monk's Trail up Doi Suthep, solo, back by 5ish."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma, running locally&lt;/strong&gt;, turns that into a plan: return time, turnaround time, how often to check in, and tips based on tonight's sunset and the forecast.&lt;/li&gt;
&lt;li&gt;You &lt;strong&gt;put your phone away.&lt;/strong&gt; That's the whole point. You only need the phone again if a notification asks &lt;em&gt;"Are you OK?"&lt;/em&gt;, and you answer it from the lock screen with one tap.&lt;/li&gt;
&lt;li&gt;If you miss a check-in, TrailWatch nudges you, waits a grace period, then alerts your emergency contact. First it sends a plain template message instantly. Then it sends a situation brief written by Gemma from the trip log. When you get signal back and tap &lt;strong&gt;I'm OK&lt;/strong&gt;, your contact gets an all-clear.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The screen time for a whole hike is about thirty seconds: one sentence before you go, and maybe one tap on the trail.&lt;/p&gt;

&lt;p&gt;The interesting part isn't the AI. It's this: &lt;strong&gt;a safety timer that lives in a normal process is a safety timer that can silently die.&lt;/strong&gt; A laptop sleeps, a server reboots, a phone's battery saver kills your app. The one moment the system &lt;em&gt;must&lt;/em&gt; work is when you're out of signal and nobody's watching. So I built TrailWatch so the timers can't be lost, and then I tried hard to break it.&lt;/p&gt;

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

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

&lt;p&gt;That's one real run, recorded in a single take. Local Gemma plans the trip, Temporal runs the workflow, and real pushes go through ntfy.sh. There are two tricks, and both are shown on screen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The clock is compressed.&lt;/strong&gt; One "trip minute" is 2 seconds, so a 40-minute walk fits in under two minutes. It's the same code path as real time; only the timer lengths are scaled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The worker-is-dead stretch plays at 4× speed&lt;/strong&gt; (labelled in the corner). Nothing interesting happens there, and that's the point.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The voice-over is &lt;strong&gt;Piper&lt;/strong&gt;, an open-source text-to-speech model, running offline on the same laptop.&lt;/p&gt;

&lt;p&gt;What happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gemma arms the trip: check in every 15 min, grace 15 min.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I hard-kill the worker&lt;/strong&gt; (&lt;code&gt;TerminateProcess&lt;/code&gt;, no cleanup). Nothing is running TrailWatch code.&lt;/li&gt;
&lt;li&gt;The phone view notices and says so: &lt;em&gt;your timers live on the Temporal server and are still running.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;The check-in deadline passes. Then the grace period passes. The alert is now overdue, and there is still no worker.&lt;/li&gt;
&lt;li&gt;I restart the worker. Temporal replays the history, sees both timers already fired, nudges the hiker and alerts the contact. The alert went out &lt;strong&gt;about 10 seconds after the worker came back.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Gemma's brief arrives, the "hiker" taps &lt;strong&gt;I'm OK&lt;/strong&gt;, and the contact gets the all-clear.&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%2F2m351p4j7rg990dyjuq9.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%2F2m351p4j7rg990dyjuq9.png" alt="Split screen: the phone view, the kill console, real ntfy pushes and the live Temporal event history" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's the event history for that run in the Temporal UI. Event 64 is the "I'm home" signal from the phone. Event 68 is the last durable timer being cancelled because of it. Every &lt;code&gt;send_notice&lt;/code&gt; result says &lt;code&gt;pushed to https://ntfy.sh/...&lt;/code&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%2Fu47f0twldpbyc0gstkr3.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%2Fu47f0twldpbyc0gstkr3.png" alt="Temporal UI event history for the run" width="799" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a real brief Gemma 3 4B wrote for the contact during an earlier kill-test run, unedited:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Ali is overdue from a solo hike up the ridge behind campus, which was scheduled to return by 23:54. Ali started the hike at 23:14 and was last heard from at 23:14. Their last known location is unknown. Sam, please attempt to contact Ali immediately. If you do not reach Ali within 15 minutes, contact local emergency services.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(Ali and Sam are demo personas.)&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/syncaimain" rel="noopener noreferrer"&gt;
        syncaimain
      &lt;/a&gt; / &lt;a href="https://github.com/syncaimain/trailwatch" rel="noopener noreferrer"&gt;
        trailwatch
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A hiking check-in timer that can't forget about you: local Gemma + Temporal durable workflows + ntfy
    &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;TrailWatch&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A hiking check-in timer that can't forget about you.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Tell it where you're going and when you'll be back, in your own words. A local
Gemma model turns that into a plan: return time, turnaround time, how often to
check in, and safety tips based on sunset and the forecast. Then you put your
phone away.&lt;/p&gt;
&lt;p&gt;Each trip is a &lt;a href="https://temporal.io" rel="nofollow noopener noreferrer"&gt;Temporal&lt;/a&gt; workflow. It sleeps on durable
timers until a check-in is due. If you don't tap &lt;strong&gt;I'm OK&lt;/strong&gt;, it nudges you
waits a grace period, then alerts your emergency contact: first with a template
message (instantly, no AI on the critical path), then with a situation brief
written by local Gemma. When you surface and tap OK, your contact gets an
all-clear.&lt;/p&gt;
&lt;p&gt;Kill the worker, restart the web app, reboot the Temporal server mid-hike: the
timer still fires.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TrailWatch is a prototype. It is not a replacement for…&lt;/p&gt;
&lt;/blockquote&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/syncaimain/trailwatch" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The safety logic is in two files: &lt;code&gt;trailwatch/workflows.py&lt;/code&gt; (the durable workflow) and &lt;code&gt;trailwatch/rules.py&lt;/code&gt; (the deterministic rules the model can't override). &lt;code&gt;pytest&lt;/code&gt; runs 12 tests. &lt;code&gt;scripts/kill_test.py&lt;/code&gt; reproduces the video on your machine.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Gemma 3 4B via &lt;strong&gt;Ollama&lt;/strong&gt;, on a laptop with a 6 GB RTX 3050 · &lt;strong&gt;Temporal&lt;/strong&gt; (Python SDK, local dev server) · &lt;strong&gt;ntfy&lt;/strong&gt; for push notifications with action buttons · &lt;strong&gt;Open-Meteo&lt;/strong&gt; for sunset and weather · FastAPI and one HTML file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;phone (web page or ntfy lock-screen buttons)
   │  start trip · I'm OK (signal) · +30 min (update) · SOS · I'm home
   ▼
FastAPI (stateless) ──► Temporal server ◄── worker
                         one HikeWorkflow per trip
                         ├─ fetch_conditions  Open-Meteo sunset + weather
                         ├─ plan_trip         local Gemma → rules clamp it
                         ├─ durable timers    check-in / grace / re-alert
                         ├─ send_notice       ntfy push, retried until it lands
                         └─ write_briefing    Gemma situation brief
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  One workflow per trip
&lt;/h3&gt;

&lt;p&gt;Every trip is a Temporal workflow, and every Temporal feature I used is doing a real job:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Temporal feature&lt;/th&gt;
&lt;th&gt;What it does in TrailWatch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Durable timers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Check-in deadlines, grace periods and re-alerts every 30 min. They live on the server, not in my process.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Signals&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;I'm OK, SOS, I'm home and Cancel, sent from the web page or straight from ntfy's lock-screen buttons.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Update + validator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"+30 min" returns the new return time to the phone. The validator rejects nonsense like +500 min, or more than 6 h total.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The web page reads live trip state from the workflow itself. There is no database.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Activity retries&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gemma cold starts, out-of-memory errors and bad JSON retry with backoff, then fall back to a regex planner. Pushes retry for up to an hour.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The heart of it is a timer racing an inbox:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_wait_until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trip_min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Durable timer racing the inbox. True if a check-in arrived first.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trip_min&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_elapsed&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minute_seconds&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;wait_condition&lt;/code&gt; timeout is a durable timer. If the worker dies while it's waiting, nothing is lost. The timer fires on the server, and whichever worker comes back next picks up from exactly that line.&lt;/p&gt;

&lt;h3&gt;
  
  
  The AI is never on the alert's critical path
&lt;/h3&gt;

&lt;p&gt;Gemma does two jobs: turning a casual sentence into a plan, and writing the brief for the contact. It does &lt;strong&gt;not&lt;/strong&gt; decide whether to alert, and the first alert never waits for it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;alert itself is a template&lt;/strong&gt;, sent instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then&lt;/strong&gt;, best effort, Gemma writes the context-rich brief. If Gemma is down, the contact already has what they need.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A cold Gemma load on my laptop takes about 70 seconds. That's fine for planning at home, and unacceptable on the alert path.&lt;/p&gt;

&lt;h3&gt;
  
  
  The model proposes, the code decides
&lt;/h3&gt;

&lt;p&gt;Gemma's plan goes through &lt;code&gt;rules.py&lt;/code&gt; before anything is armed. The model can suggest anything, but it can't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set check-ins longer than 60 min on a solo trip, or 45 min on a high-risk one&lt;/li&gt;
&lt;li&gt;lower the risk level that the sunset or wind implies&lt;/li&gt;
&lt;li&gt;pick a turnaround time that leaves no time to walk back&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every rule has a test, and most of them exist because Gemma broke something.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three bugs worth telling you about
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. "Turn around at minute 1440."&lt;/strong&gt; For a three-hour hike, Gemma 3 4B proposed a turnaround time of 1440 minutes, a full day later. Fix: a turnaround has to land in the first 75% of the trip, or it's replaced with the halfway point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "Back in 40 minutes" → back at 12:39 tomorrow.&lt;/strong&gt; At 23:00, Gemma turned &lt;em&gt;"back in 40 minutes"&lt;/em&gt; into &lt;code&gt;expected_return_local: "12:39"&lt;/code&gt; and, in the same reply, &lt;code&gt;duration_min: 40&lt;/code&gt;. My parser trusted the clock time, so the hiker would have been reported missing 13 hours late. &lt;strong&gt;I only caught this because the kill-test run printed the plan.&lt;/strong&gt; Fix: when the model's clock time and duration disagree, use the &lt;strong&gt;earlier&lt;/strong&gt; one. A false alarm is better than a late one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. My own bug: downtime pushed alerts later.&lt;/strong&gt; My first version started the grace period &lt;em&gt;after the nudge was sent&lt;/em&gt;. If the worker was dead through the deadline, the nudge went out late when it restarted, the grace period started from there, and the alert came a full grace period late. Durable timers don't help if you measure from the wrong moment. Fix: grace is counted from the &lt;strong&gt;missed deadline&lt;/strong&gt;, which is fixed in the workflow's history, so downtime can never delay an alert. There's now an integration test that keeps the worker dead through both deadlines and checks that the alert fires the moment it's back.&lt;/p&gt;

&lt;p&gt;In one test, Gemma also called a 10% chance of rain "significant." That's why tips are just advice and warnings come from code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time-skipping:&lt;/strong&gt; Temporal's test server runs a &lt;strong&gt;6-hour silent hike in seconds&lt;/strong&gt;. It checks the contact gets exactly 10 alerts (one at 75 min, then every 30 min) and only one Gemma brief.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker death:&lt;/strong&gt; a real dev server with a compressed clock. The worker is shut down through the deadline and the grace period, and the test checks the alert fires on restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules:&lt;/strong&gt; real Gemma mistakes, kept as regression tests.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Because this app knows when you're alone and where you'll be.&lt;/strong&gt; A trip plan is "this person will be by themselves on this ridge until 5 pm, and nobody will be home." I don't want that in someone else's logs. With TrailWatch, the model runs on my laptop, Temporal is open source and self-hosted, ntfy can be self-hosted, and the weather comes from open data. Nothing in the safety path depends on a vendor's uptime, pricing or terms of service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because a safety tool shouldn't cost a subscription.&lt;/strong&gt; Running TrailWatch costs nothing. A small open-weight model is enough because the architecture keeps it off the critical path: it plans and explains, while code and durable timers make the decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because I could see how it fails.&lt;/strong&gt; Running Gemma locally, I could hammer it, watch it make the 1440-minute mistake and the 12:39 mistake, and turn each into a rule and a test. Swapping models is one environment variable: any Ollama model, or any OpenAI-compatible endpoint, including hosted Gemma.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where a closed API would have been better:&lt;/strong&gt; honestly, speed and judgment. A big hosted model would plan in about 2 seconds instead of 10–35, and would probably not suggest turning around tomorrow. But for this problem I'll take a model I can run, inspect and constrain over one that's merely smarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This is a prototype, not a satellite messenger or a PLB.&lt;/strong&gt; It needs your phone to get a push now and then. Losing signal is exactly the situation it alerts on, but it can't call for help from a valley by itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The video uses a compressed clock.&lt;/strong&gt; The code path is the same as real time, but it's still a demo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ntfy topics are like passwords&lt;/strong&gt; on the public server. Use long random ones, or self-host ntfy. I added an access key before putting the app behind a tunnel, so a leaked URL can't cancel your trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push delivery is at-least-once.&lt;/strong&gt; If the worker dies mid-push, a retry &lt;em&gt;can&lt;/em&gt; send a duplicate. In my runs, the push that was in flight when I killed the worker arrived exactly once, but I don't promise that.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I built TrailWatch pair-programming with an AI coding agent (Claude Code). That session is also where the bugs above were found: the agent wrote the kill test, read Gemma's real output, and flagged the grace-period bug in its own earlier code.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal&lt;/strong&gt;: each trip is a durable workflow using timers, signals, an update with a validator, queries and retry policies, with a hard-kill test on video and the event history to back it up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 3 4B runs locally through Ollama to plan trips and write situation briefs, constrained by tested rules.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Pothole Adoption Agency</title>
      <dc:creator>Faris </dc:creator>
      <pubDate>Sun, 04 Oct 2026 20:46:15 +0000</pubDate>
      <link>https://dev.to/faris-khan/kk-3ig0</link>
      <guid>https://dev.to/faris-khan/kk-3ig0</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 Two: Vibe-Code Something Strange&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every pet in this shelter is a real pothole complaint filed with NYC 311 in Queens Community Board 13. Its fate follows&lt;br&gt;
the city's own record: repaired, "no pothole found", transferred, or still waiting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo (no login):&lt;/strong&gt; &lt;a href="https://pothole-shelter.vercel.app" rel="noopener noreferrer"&gt;https://pothole-shelter.vercel.app&lt;/a&gt; · &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/syncaimain/pothole-shelter" rel="noopener noreferrer"&gt;https://github.com/syncaimain/pothole-shelter&lt;/a&gt; ·&lt;br&gt;
&lt;strong&gt;Sanity project:&lt;/strong&gt; &lt;code&gt;fixjy07h&lt;/code&gt; (dataset &lt;code&gt;production&lt;/code&gt;, public-read)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measured on the live dataset, 4 October 2026&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Complaints synced&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2,512&lt;/strong&gt;: every Queens CB 13 street pothole from the last 180 days, plus every one still open. Nothing sampled.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lost strays (no coordinates in the record)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,555 (62%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outcomes&lt;/td&gt;
&lt;td&gt;668 adopted · 409 ghosts · 449 transferred · &lt;strong&gt;963 feral&lt;/strong&gt; · 18 in the shelter · 5 unmapped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median time from report to repair&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.9 days&lt;/strong&gt; (653 repaired complaints)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unmapped resolution phrases&lt;/td&gt;
&lt;td&gt;1 phrase, 5 pets, shown publicly instead of guessed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second sync over the same data&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0 created · 0 updated · 0 status events&lt;/strong&gt; (incremental &lt;em&gt;and&lt;/em&gt; forced full)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflows (Sanity Workflows engine)&lt;/td&gt;
&lt;td&gt;3 definitions deployed · 981 live lifecycle instances · 366 cluster reviews · adoption moderation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests, all in CI&lt;/td&gt;
&lt;td&gt;98 unit and workflow tests (20 on the Workflows test bench) · 26 Playwright judge-path + axe tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A shelter where the animals are potholes. Browse them, read their stories, follow the one that's been waiting since 2021,&lt;br&gt;
visit the ghost hall, and leave one an adoption note. It's for anyone who has ever reported a pothole and wondered what&lt;br&gt;
happened next, and for anyone who likes their civic data with a bit of heart.&lt;/p&gt;

&lt;p&gt;The rule I never broke: &lt;strong&gt;nothing about a pet is made up, and nothing about its fate is decided by me.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The city record decides.&lt;/strong&gt; A deterministic sync maps each 311 record to an outcome. Closed complaints are matched
&lt;em&gt;exactly&lt;/em&gt; against &lt;code&gt;resolutionMapping&lt;/code&gt; documents that a person wrote, one per phrase the city actually uses. Wording
nobody has mapped goes to a public &lt;strong&gt;Unmapped&lt;/strong&gt; bucket instead of being guessed. Open complaints turn &lt;strong&gt;feral&lt;/strong&gt; after 60 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data's own gaps become characters.&lt;/strong&gt; 62% of complaints have no coordinates: they describe a stretch of street
between two cross streets, not a point. Those are the &lt;strong&gt;lost strays&lt;/strong&gt;, listed by street and never placed on a map I'd
have to invent. When an inspector finds nothing, the pet becomes a &lt;strong&gt;ghost&lt;/strong&gt;. The 963 ferals are the city's real backlog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bios restate facts only.&lt;/strong&gt; Each bio starts from a fact template: the date it was reported, the street, and the city's
resolution note &lt;em&gt;quoted verbatim&lt;/em&gt;. Gemini may restyle it, but a fact guard rejects any changed date, number or street, a
reworded city note, or anything that mocks people. House numbers never appear, and coordinates are rounded to about 100 m.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Celebrate repairs, never mock anyone.&lt;/strong&gt; The joke is the shelter, not the people who report potholes or the crews who
fix them. When the Department of Transportation repairs one, the pet is "adopted into forever pavement" and the street
gets a congratulations.&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%2Fo13d1qc7lwyr67k9e1cq.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%2Fo13d1qc7lwyr67k9e1cq.png" alt="The Pothole Adoption Agency gallery: shelter totals by outcome above a grid of pet cards, each showing the pet's name, outcome badge, street and report date" width="800" height="500"&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%2Fci0h4ol36c8as47zo4o0.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%2Fci0h4ol36c8as47zo4o0.png" alt="The Feral of the week card: one long-waiting pothole, picked by the calendar week, with how long its city record has been open" width="797" height="121"&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://pothole-shelter.vercel.app" rel="noopener noreferrer"&gt;https://pothole-shelter.vercel.app&lt;/a&gt; (no login anywhere on the public site)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/y3JjbC-fPN8" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The narration is synthetic: each caption line was voiced with Gemini TTS (&lt;code&gt;gemini-3.8-flash-tts&lt;/code&gt;) by &lt;code&gt;docs/media/make-voiceover.mjs&lt;/code&gt;, so voice and captions say the same words at the same moments.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The judge path:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;a href="https://pothole-shelter.vercel.app/?outcome=feral&amp;amp;age=years" rel="noopener noreferrer"&gt;gallery&lt;/a&gt; and click a Feral pet.&lt;/li&gt;
&lt;li&gt;Read its complaint history and follow the link to the &lt;strong&gt;real 311 record&lt;/strong&gt; on NYC Open Data.&lt;/li&gt;
&lt;li&gt;Open &lt;a href="https://pothole-shelter.vercel.app/strays" rel="noopener noreferrer"&gt;lost strays&lt;/a&gt; and the &lt;a href="https://pothole-shelter.vercel.app/ghosts" rel="noopener noreferrer"&gt;ghost hall&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Leave an adoption note on any shelter or feral pet. You'll see "awaiting moderation". The note stays private until a
person approves it in the Shelter Office.&lt;/li&gt;
&lt;li&gt;Open the &lt;a href="https://pothole-shelter.vercel.app/sync" rel="noopener noreferrer"&gt;sync log&lt;/a&gt; for the last sync time, the run log and the unmapped phrases.&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%2Fqvdeu42ia682mkodo5o0.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%2Fqvdeu42ia682mkodo5o0.png" alt="A feral pet's page: its name, outcome and temperament badges, a bio that restates only facts from its 311 record, its street and community board, and a map pin rounded to about 100 metres" width="800" height="500"&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%2Fojyyszhduytd2779sfid.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%2Fojyyszhduytd2779sfid.png" alt="A pet's status timeline, where every change cites the 311 field that caused it, above its complaint history with the city's resolution note quoted word for word" width="800" height="500"&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%2Faj8n1jlqi43v7x0c0zp3.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%2Faj8n1jlqi43v7x0c0zp3.png" alt="The pet's original NYC 311 record as raw JSON from the NYC Open Data API, opened from the link in its complaint history" width="800" height="500"&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%2Fhcaudx713jmui7fbimrb.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%2Fhcaudx713jmui7fbimrb.png" alt="Map of every pet with coordinates in Queens Community Board 13: dots coloured by outcome, running along the Queens–Nassau border" width="800" height="500"&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%2Flm9g16udk1826wmr5f4x.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%2Flm9g16udk1826wmr5f4x.png" alt="Lost strays on one street: complaints the city recorded without coordinates, listed by street and cross street instead of being placed on the map" width="800" height="500"&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%2Fxwhcau4b7m18szu4i6nc.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%2Fxwhcau4b7m18szu4i6nc.png" alt="The ghost hall: cards for pets whose reported pothole the city inspected and did not find" width="800" height="500"&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%2Frlg534os0mato5vupsk6.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%2Frlg534os0mato5vupsk6.png" alt="The adoption form for a shelter pet: a name and a short note, which stays private until a person approves it" width="800" height="500"&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%2Fw1sy4uiapxvmqc5cmcsx.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%2Fw1sy4uiapxvmqc5cmcsx.png" alt="The sync log: the last successful sync time and a table of recent runs with fetched, new, changed and status-event counts" width="800" height="500"&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%2Fkkp4a2ozq3jdkgqzld9x.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%2Fkkp4a2ozq3jdkgqzld9x.png" alt="The public read-only mirror of the Shelter Office: moderation and review queues, live workflow instances by stage, sync health and the lifecycle board" width="800" height="991"&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%2Fgd69nqfljpseqh4xq86p.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%2Fgd69nqfljpseqh4xq86p.png" alt="The gallery on a phone: filters and pet cards in a single column" width="800" height="1731"&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%2Fw5hxr95msk67ko6l2ub1.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%2Fw5hxr95msk67ko6l2ub1.png" alt="A pet's page on a phone: its bio, badges and rounded map pin" width="800" height="1731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing the Shelter Office (App SDK, needs a Sanity login):&lt;/strong&gt; there's no shared test account. The video shows a note being approved in the Shelter Office at 2:18, and every Office view has a public read-only mirror at &lt;a href="https://pothole-shelter.vercel.app/office" rel="noopener noreferrer"&gt;/office&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/syncaimain" rel="noopener noreferrer"&gt;
        syncaimain
      &lt;/a&gt; / &lt;a href="https://github.com/syncaimain/pothole-shelter" rel="noopener noreferrer"&gt;
        pothole-shelter
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &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;Pothole Adoption Agency&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/syncaimain/pothole-shelter/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/syncaimain/pothole-shelter/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://github.com/syncaimain/pothole-shelter/actions/workflows/sync.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/syncaimain/pothole-shelter/actions/workflows/sync.yml/badge.svg" alt="311 sync"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Real NYC 311 pothole complaints from Queens Community Board 13, turned into adoptable pets whose fates follow the city's
own record: repaired, "no pothole found", transferred, or still waiting. Built for the Sanity Challenge on DEV, Path Two.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live site:&lt;/strong&gt; &lt;a href="https://pothole-shelter.vercel.app" rel="nofollow noopener noreferrer"&gt;https://pothole-shelter.vercel.app&lt;/a&gt; (no login)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanity project ID:&lt;/strong&gt; &lt;code&gt;fixjy07h&lt;/code&gt;, dataset &lt;code&gt;production&lt;/code&gt;, public-read. Example queries are in &lt;a href="https://github.com/syncaimain/pothole-shelter/docs/links/public-queries.txt" rel="noopener noreferrer"&gt;docs/links/public-queries.txt&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build log&lt;/strong&gt; (the honest version): &lt;a href="https://github.com/syncaimain/pothole-shelter/docs/BUILD_LOG.md" rel="noopener noreferrer"&gt;docs/BUILD_LOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What a judge can check&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Open the gallery and click a Feral pet.&lt;/li&gt;
&lt;li&gt;Read its complaint history and follow a link to the real 311 record on NYC Open Data.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;/strays&lt;/code&gt; (complaints with no coordinates) and &lt;code&gt;/ghosts&lt;/code&gt; (inspected, no defect found).&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;/sync&lt;/code&gt; for the last sync time, the run log, and the unmapped resolution phrases.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Numbers (live dataset, 4 October 2026)&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;


&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Complaints synced&lt;/td&gt;
&lt;td&gt;2,512 (every Queens CB 13 street pothole from the last 180 days, plus every one still open)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/syncaimain/pothole-shelter" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;A pnpm workspace: &lt;code&gt;studio/&lt;/code&gt; (schema and desk structure), &lt;code&gt;web/&lt;/code&gt; (Next.js 16 public site), &lt;code&gt;sync/&lt;/code&gt; (311 → Sanity, with the&lt;br&gt;
outcome rules and clustering), &lt;code&gt;workflows/&lt;/code&gt; (definitions and bench tests), &lt;code&gt;app/&lt;/code&gt; (the App SDK Shelter Office), &lt;code&gt;agent/&lt;/code&gt;&lt;br&gt;
(Gemini bio writer and fact guard) and &lt;code&gt;ingest/&lt;/code&gt; (discovery, migrations, and every raw 311 snapshot with its SHA-256).&lt;br&gt;
CI runs typecheck, lint, all tests, a snapshot checksum check, both builds, and the Playwright judge path with axe-core.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Build Process
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;IDE and agent:&lt;/strong&gt; I built this with &lt;strong&gt;Claude Code&lt;/strong&gt; (Claude Opus 5.5) in a terminal, driven by prompts and a written spec.&lt;br&gt;
A second coordinating session sent guidance through &lt;code&gt;docs/GUIDANCE.md&lt;/code&gt;, and the building agent reported back in&lt;br&gt;
&lt;code&gt;docs/REPORT.md&lt;/code&gt;. Both files are in the repo, unedited, along with the full log in &lt;code&gt;docs/BUILD_LOG.md&lt;/code&gt;. The app itself&lt;br&gt;
uses &lt;strong&gt;Gemini&lt;/strong&gt; (&lt;code&gt;gemini-3.7-flash&lt;/code&gt;) for bios.&lt;/p&gt;

&lt;h3&gt;
  
  
  The prompt that set the tone
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Read the spec in full. Then, in order: verify the Gemini key with one cheap call (if it fails, stop and report); confirm&lt;br&gt;
every item marked "Verify" against current live docs; scaffold the pnpm workspace. Stop there and report before writing&lt;br&gt;
feature code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Stop and report" was the most useful instruction I gave. Several of the spec's own facts were wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Workflows package names were partly wrong.&lt;/strong&gt; The CLI binary is &lt;code&gt;sanity-workflows&lt;/code&gt;, it needs
&lt;code&gt;@sanity/workflow-blueprint&lt;/code&gt;, and the test bench lives in &lt;code&gt;@sanity/workflow-engine-test&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript had to be pinned to 6.0.3,&lt;/strong&gt; not the 7.0.2 that &lt;code&gt;latest&lt;/code&gt; gave, nor the 5.x one guide suggested. It's the only
version that satisfies both &lt;code&gt;typescript-eslint&lt;/code&gt; (&amp;lt;6.1) and the Workflows blueprint (≥6.0.3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16 ships an &lt;code&gt;AGENTS.md&lt;/code&gt; that says "This is NOT the Next.js you know"&lt;/strong&gt; — and it isn't. Caching is now
&lt;code&gt;cacheComponents&lt;/code&gt; + &lt;code&gt;'use cache'&lt;/code&gt;. The first build failed because "Feral of the week" read the clock outside a cached scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel's Hobby plan only allows daily crons,&lt;/strong&gt; so the sync is scheduled hourly on GitHub Actions instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Messy data, decided honestly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; Queens had ~17,000 street-pothole complaints in 180 days, plus ~8,800 still open. Rather than sample, I took
&lt;strong&gt;one community board in full&lt;/strong&gt;, so every pet is traceable and clustering sees complete data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing coordinates:&lt;/strong&gt; 62% of complaints have no point. I chose not to geocode them, because that would invent
precision the record doesn't have. They became the lost strays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Referred" resolutions&lt;/strong&gt; never sit on a closed ticket in CB 13; they're all Pending. The status rule stays in charge
rather than a mapping overriding a ticket the city still lists as open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unclear resolution text:&lt;/strong&gt; "The status of this Service Request is currently not available online…" is the whole
Unmapped bucket. It's shown publicly rather than guessed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The median time to close is misleading.&lt;/strong&gt; It's 0.4 days overall because 366 duplicates closed within one minute of
being filed. I quote the repair median (0.9 days) instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;91% of located complaints have no &lt;code&gt;street_name&lt;/code&gt;&lt;/strong&gt;: they're intersections, with only &lt;code&gt;intersection_street_1/2&lt;/code&gt;. A
clustering rule keyed on &lt;code&gt;street_name&lt;/code&gt; would silently have skipped them. A test caught it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"0 m apart" means "the same corner",&lt;/strong&gt; not "the same pothole": intersection complaints all share the intersection's
point. The Shelter Office tells reviewers so.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where it got stuck, and how it course-corrected
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dotted document IDs.&lt;/strong&gt; The spec's own example (&lt;code&gt;complaint311.&amp;lt;key&amp;gt;&lt;/code&gt;) would have hidden every document from judges,
because Sanity never serves dotted IDs to anonymous readers. I switched to hyphens. Later I used the &lt;em&gt;same&lt;/em&gt; behaviour on
purpose: pending adoption notes get a dotted ID, so nothing a visitor types is public before a person approves it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One document per status event put me over the plan's cap.&lt;/strong&gt; 4,992 event documents took the dataset to 10,022 of
10,000. Events now live as an append-only array inside each pet. I migrated by &lt;strong&gt;moving&lt;/strong&gt; every event verbatim,
verified 4,992/4,992 in place, and only then deleted. The dataset fell to about 5,000 documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$actor.kind == "person"&lt;/code&gt; gated nothing.&lt;/strong&gt; The tests said people could approve, but they also said agents could. A
probe showed the Workflows engine stamps &lt;em&gt;every&lt;/em&gt; resolved actor as kind "person", robots included. What actually
discriminates is the id namespace (&lt;code&gt;g…&lt;/code&gt; user, &lt;code&gt;p-…&lt;/code&gt; robot token). The bug stayed hidden because my test fixtures used
fake ids that happened to look like robots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I said "nothing was written" when something was.&lt;/strong&gt; A migration step was interrupted, the tool reported the call as
rejected, and the agent told me nothing had been appended. It had. The agent found this itself on the next run and
corrected the record. Lesson: check the data before stating what didn't happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stopping a shell doesn't stop its process.&lt;/strong&gt; Two long backfills were cut off at a time limit, but their processes kept
running and raced the re-runs, creating 183 duplicate workflow instances. The agent traced them by timestamp, added an
optimistic-lock claim (&lt;code&gt;ifRevisionID&lt;/code&gt;) so it can't recur, and &lt;strong&gt;aborted&lt;/strong&gt; the duplicates through the engine with a
recorded reason rather than deleting them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three different pets were all called "Arepa Asphaltine".&lt;/strong&gt; 800 base names for 2,512 pets. This was found by
&lt;em&gt;reading&lt;/em&gt; the first Gemini bios instead of trusting that they had passed the guard. Pets sharing a base name now take
pedigree ordinals (II, III…) in complaint-key order, and a newer pet never renames an older one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;axe-core found a real accessibility bug:&lt;/strong&gt; on phones the sync tables scroll sideways, but the scroll region wasn't
keyboard-focusable. Fixed, and now tested on every push.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reaching past the Studio: Workflows and the App SDK
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Workflows.&lt;/strong&gt; The spec asked for a 2-hour spike with a declared fallback. The spike &lt;strong&gt;succeeded&lt;/strong&gt;, and nothing fell back.&lt;br&gt;
Three definitions were validated offline and tested on the in-memory bench, including the 60-day Feral rule on a fixed&lt;br&gt;
clock: still in the shelter one second before 60 days, feral at exactly 60. They're deployed from a &lt;code&gt;sanity login&lt;/code&gt; session.&lt;br&gt;
A project-scoped token deploys fine and then fails on a cross-resource read, exactly as the docs warn.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pothole-lifecycle&lt;/code&gt;: Reported → In the shelter → Adopted · Ghost · Transferred · Feral · &lt;strong&gt;Unmapped&lt;/strong&gt;. I added the
Unmapped stage; without it, a closed-but-unmapped pet would wrongly turn feral by the clock. The sync (a robot token) is
the only caller that can record an outcome, and it ticks the clock-based transitions itself. There's one live instance
per open pet: 981 right now, matching the pets' outcomes exactly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cluster-review&lt;/code&gt;: Proposed → Approved · Rejected. The rule proposes, and a person decides in the Shelter Office. The sync
merges only clusters whose decider is a person.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;adoption-moderation&lt;/code&gt;: Submitted → Approved · Rejected, decided by a person.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two lessons the docs didn't spell out. Engine gates are &lt;strong&gt;advisory&lt;/strong&gt;: the actor is "provenance, not an authenticated&lt;br&gt;
principal", and mutation guards aren't enforced by the lake yet. So the real rules also live in code I control. And&lt;br&gt;
instance documents are &lt;strong&gt;private&lt;/strong&gt; (dotted IDs), so the public mirror reads workflow state through a server-side proxy that&lt;br&gt;
publishes totals only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App SDK.&lt;/strong&gt; The &lt;strong&gt;Shelter Office&lt;/strong&gt; is a custom app in the Sanity Dashboard, with real-time data and its own interface: live&lt;br&gt;
queues to moderate adoption notes and review cluster merges, the unmapped phrases, sync health, and a live lifecycle board.&lt;br&gt;
Approving a note fires the moderation workflow &lt;em&gt;as the signed-in person&lt;/em&gt;. The engine records who decided from their&lt;br&gt;
token, and only then is a public copy of the note written. Every Office view has a public read-only mirror at&lt;br&gt;
&lt;a href="https://pothole-shelter.vercel.app/office" rel="noopener noreferrer"&gt;/office&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini, behind a fact guard.&lt;/strong&gt; &lt;code&gt;gemini-3.7-flash&lt;/code&gt; (version &lt;code&gt;3.7-flash-08-2026&lt;/code&gt;, recorded on every bio it writes)&lt;br&gt;
restyles template bios into the shelter's voice. The guard accepted 65 of 65 in the sample run. Reasoning models eat&lt;br&gt;
output tokens: a two-letter test reply used 80 reasoning tokens, so bios get a 4,096-token budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I cut
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All of Queens.&lt;/strong&gt; One community board in full beat a sample of the whole borough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geocoding strays.&lt;/strong&gt; It would have put invented points on a map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filing new complaints.&lt;/strong&gt; Out of scope by design: this shelter only reflects the city's record.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Known limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resolution wording is inconsistent,&lt;/strong&gt; and five closed complaints say nothing useful. They stay Unmapped until a person decides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clustering is a heuristic&lt;/strong&gt; (60 m / 30 days / same street; strays by same block). That's why every merge needs a person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;311 dates have no time zone.&lt;/strong&gt; I read them as New York local time. The data is consistent with that, but it's an assumption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hourly schedule is best-effort.&lt;/strong&gt; GitHub Actions delays scheduled runs under load and sometimes skips them: on
4 October the 19:17 UTC slot ran 22 minutes late and the 20:17 slot didn't run. Every run, scheduled or manual, is
idempotent, and the &lt;a href="https://pothole-shelter.vercel.app/sync" rel="noopener noreferrer"&gt;sync log&lt;/a&gt; shows real run times, never the schedule.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why it's new
&lt;/h3&gt;

&lt;p&gt;Most civic-data projects turn records into dashboards. This one turns them into characters, and lets the data's gaps play&lt;br&gt;
the parts: the complaints with no coordinates become lost strays, the ones the city looked for and couldn't find become&lt;br&gt;
ghosts, and the backlog becomes a feral wing that's still waiting. Every fate is the city's own, and every step can be&lt;br&gt;
traced back to a real record.&lt;/p&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;fixjy07h&lt;/code&gt; · &lt;strong&gt;dataset:&lt;/strong&gt; &lt;code&gt;production&lt;/code&gt; (public-read, no token needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public query:&lt;/strong&gt; &lt;a href="https://fixjy07h.api.sanity.io/v2025-02-19/data/query/production?query=*%5B_type==%22pothole%22%5D%5B0...5%5D" rel="noopener noreferrer"&gt;https://fixjy07h.api.sanity.io/v2025-02-19/data/query/production?query=*[_type=="pothole"][0...5]&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Schema, and why raw records and pet fields are kept apart:&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;Type&lt;/th&gt;
&lt;th&gt;What it holds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;complaint311&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The 311 row &lt;strong&gt;exactly as published&lt;/strong&gt; (&lt;code&gt;raw&lt;/code&gt;), its SHA-256, and the source row URL. Rewritten only when the hash changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pothole&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The pet: name, temperament, bio (with its model and fact-guard provenance), rounded location, outcome (written only by the sync), its lifecycle workflow instance, and &lt;code&gt;events&lt;/code&gt;, an append-only history where every entry cites the complaint field that caused it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resolutionMapping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One exact city phrase → outcome, with a person's rationale.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clusterDecision&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A proposed merge, with its review workflow and the person who decided.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;adoption&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A visitor's note: private while pending, public only once a person approves it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;syncRun&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every sync: counts, watermark, and the SHA-256 of each raw page it saved.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keeping the city's record and my interpretation in separate documents means anyone can check any pet against the 311 row&lt;br&gt;
it came from, and a re-sync can never overwrite a decision a person made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data:&lt;/strong&gt; &lt;a href="https://data.cityofnewyork.us/Social-Services/311-Service-Requests-from-2020-to-Present/erm2-nwe9" rel="noopener noreferrer"&gt;311 Service Requests from 2020 to Present&lt;/a&gt;,&lt;br&gt;
NYC Open Data, dataset &lt;code&gt;erm2-nwe9&lt;/code&gt;. The City of New York does not vouch for the accuracy or completeness of this site or&lt;br&gt;
the data it uses, which has been modified for use from its original source.&lt;/p&gt;

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