<?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: Thomas Lee</title>
    <description>The latest articles on DEV Community by Thomas Lee (@tomlee).</description>
    <link>https://dev.to/tomlee</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%2F4025134%2F8985b855-e470-402b-96ca-619af3b61fc0.png</url>
      <title>DEV Community: Thomas Lee</title>
      <link>https://dev.to/tomlee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tomlee"/>
    <language>en</language>
    <item>
      <title>Alpha to Beta: Bringing In QA</title>
      <dc:creator>Thomas Lee</dc:creator>
      <pubDate>Fri, 24 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/tomlee/alpha-to-beta-bringing-in-qa-1fch</link>
      <guid>https://dev.to/tomlee/alpha-to-beta-bringing-in-qa-1fch</guid>
      <description>&lt;p&gt;&lt;a href="https://lee.yt/posts/a-team-of-one-running-two-teams/" rel="noopener noreferrer"&gt;Two posts ago&lt;/a&gt;, R and D were two roles, two rulebooks, one person switching hats. &lt;a href="https://lee.yt/posts/the-devops-team-that-never-sleeps/" rel="noopener noreferrer"&gt;Last time&lt;/a&gt; was D's story: the loop that made shipping safe. By now the two don't split cleanly — every decision and every build happen in the same conversation. This round adds a third: &lt;strong&gt;QA&lt;/strong&gt;, an external auditor with no stake in the code already being good, brought in specifically to check R and D's work from the outside.&lt;/p&gt;

&lt;p&gt;Getting something to alpha proves it works. That's not the same question as beta. Beta is four separate things you have to prove, honestly, about the same piece of software:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it hold up to scrutiny you didn't supply yourself?&lt;/li&gt;
&lt;li&gt;Can it get more capable without spending down the trust its first design earned?&lt;/li&gt;
&lt;li&gt;Does it survive contact with real use — not just the tests you thought to write — and is it actually pleasant to use?&lt;/li&gt;
&lt;li&gt;Can people rely on explicit promises about what will and won't change under them?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the checklist. Here's how it played out over the following two weeks, building &lt;a href="https://omnist.dev" rel="noopener noreferrer"&gt;Omnist&lt;/a&gt;, a &lt;a href="https://omnist.dev/why-omnist/" rel="noopener noreferrer"&gt;schema library&lt;/a&gt; I work on in my own time. The point isn't Omnist — it's the moves. Omnist just supplies the proof they're necessary.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Invite an outside review, and argue back where it's wrong.&lt;/li&gt;
&lt;li&gt;Add capability without spending down what makes you trustworthy.&lt;/li&gt;
&lt;li&gt;Re-prove your core guarantees survived the change.&lt;/li&gt;
&lt;li&gt;Stress-test it against the real world, not just your own tests.&lt;/li&gt;
&lt;li&gt;Make it easier to use without touching what makes it correct.&lt;/li&gt;
&lt;li&gt;Make explicit, durable promises about what won't change.&lt;/li&gt;
&lt;li&gt;Keep the habit running after the deadline is gone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: Invite an outside review, and argue back where it's wrong
&lt;/h2&gt;

&lt;p&gt;Self-review has a blind spot, and it's a specific, well-known one: the &lt;a href="https://en.wikipedia.org/wiki/Inattentional_blindness" rel="noopener noreferrer"&gt;selective-attention effect&lt;/a&gt; where you miss what you weren't looking for, even when it's right in front of you. The AI that wrote the code has the same blind spot as any author — its own assumptions are invisible to it precisely because they're its assumptions. It can't flag its own hallucinations as hallucinations, and it can't see its own style as a choice rather than just "how the code looks."&lt;/p&gt;

&lt;p&gt;So bring in scrutiny you don't control — a reviewer with no stake in the code already being good, and no shared blind spots with the one who wrote it. That's QA's whole job: a different AI, not R or D, brought in to audit Omnist specifically to catch what the builder couldn't see about its own work — confidently invented claims, code that worked but read badly, patterns the original author was too close to question. Expect real findings mixed with noise, and expect some of the real findings to be inconvenient — QA's first pass came back with a verdict of "production ready." It wasn't, not yet, and I said so. Same rule this whole series runs on, &lt;a href="https://lee.yt/posts/a-team-of-one-running-two-teams/" rel="noopener noreferrer"&gt;going back to the first post&lt;/a&gt;: disagreement owes a reason, not a shrug.&lt;/p&gt;

&lt;p&gt;Then triage before you fix. Whatever's loudest first is how you end up chasing noise instead of what matters. A tracked queue, not a reflex. A few real fixes came out of Omnist's: safer behavior on bad input instead of a silent fallback, clearer error messages instead of a loose string, dependencies pinned instead of left floating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add capability without spending down what makes you trustworthy
&lt;/h2&gt;

&lt;p&gt;At some point your first design won't cover a case real users actually have. The instinct is to loosen the rule everywhere it might be needed. Resist that. Loosen it in exactly one place, explicitly marked, never automatic. The value of a strict design is what it lets you promise — give that up broadly and you've quietly downgraded the whole thing to best-effort.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://omnist.dev/schema/" rel="noopener noreferrer"&gt;Omnist's schema model&lt;/a&gt; started fully closed: every field exactly one type, no exceptions. That's what let it answer real questions — &lt;a href="https://omnist.dev/api/" rel="noopener noreferrer"&gt;is this change safe, are these two schemas the same&lt;/a&gt; — with an actual answer instead of a guess. Real data didn't always fit. The fix wasn't loosening everywhere; it was one narrow, explicit escape hatch (what the project calls &lt;a href="https://omnist.dev/design/any-type-spec/" rel="noopener noreferrer"&gt;&lt;code&gt;any&lt;/code&gt;&lt;/a&gt;), usable only where marked, with its cost written down before any code changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Re-prove your core guarantees survived the change
&lt;/h2&gt;

&lt;p&gt;Any time you loosen a strict system, you owe yourself proof the strictness that's left still works. Not a hope — an actual re-check, done by someone who didn't write the change. That's the same never-self-verify rule from &lt;a href="https://lee.yt/posts/the-devops-team-that-never-sleeps/" rel="noopener noreferrer"&gt;the loop that made shipping safe&lt;/a&gt;, just applied to a riskier kind of change. Ship the loosening in pieces small enough that each one can be verified before the next goes out.&lt;/p&gt;

&lt;p&gt;Done properly, that check isn't a formality. On Omnist it caught a real infrastructure bug early, and later something subtler: a test that had quietly stopped testing what it claimed to, after an unrelated change shifted what it actually exercised. It still passed. Only someone checking the real behavior — not trusting the test's own claim about itself — caught it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Stress-test it against the real world, not just your own tests
&lt;/h2&gt;

&lt;p&gt;Your own test suite only covers what you thought to check. The harder, more honest test is real use you didn't design for: real files from outside your own project, not synthetic cases built to be easy.&lt;/p&gt;

&lt;p&gt;Running Omnist against &lt;a href="https://omnist.dev/examples/" rel="noopener noreferrer"&gt;real external file formats&lt;/a&gt; surfaced things self-testing never would have. One format had no real specification behind it at all, which made everything checked against it weaker than the field count alone would suggest. An unrelated parsing rule broke real files outright, for a reason that had nothing to do with the logic being tested. And the single most rigorously specified format found a limit none of the messier ones could: checking that a value has the right type isn't the same as checking it's in the right range. Four honest limits, worth writing up as guidance afterward instead of leaving as one-off surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Make it easier to use without touching what makes it correct
&lt;/h2&gt;

&lt;p&gt;Correctness earns trust. Ease of use earns adoption. You need both, and they shouldn't be the same piece of work — a usability improvement should ship without anyone having to re-verify the model underneath it.&lt;/p&gt;

&lt;p&gt;Omnist got what I've been calling &lt;a href="https://omnist.dev/formats/oml/" rel="noopener noreferrer"&gt;array sugar&lt;/a&gt;: shorthand for a pattern that used to take several lines to write out, readable at a glance instead of parsed line by line. Pure convenience — it expands and collapses automatically, and changes nothing about what the data means underneath. Even something this small went through the same discipline as everything else: spec first, a failing test before the fix, independent review after. That review still found something, not in the feature itself but in the seam between two people's pieces of it — exactly the kind of gap the discipline exists to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Make explicit, durable promises about what won't change
&lt;/h2&gt;

&lt;p&gt;Eventually you have to tell people, in writing, what they can rely on and what they can't. That's a different kind of document than a bug fix. It's a public commitment, and getting it wrong is worse than not having one — now people are relying on something imprecise.&lt;/p&gt;

&lt;p&gt;Before Omnist could honestly call itself beta, one real bug had to go: malformed input crashing instead of failing with a clear message, on exactly one side of an otherwise-guarded path. And a &lt;a href="https://omnist.dev/stability/" rel="noopener noreferrer"&gt;written policy&lt;/a&gt; had to exist — what changes only with warning, what carries no promise at all. Both went through the same review-before-merge discipline as everything else; the policy in particular got a paragraph rewritten after review found it blurring two things that needed to stay separate. The version number moved only after every claim it would make had already been checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Keep the habit running after the deadline is gone
&lt;/h2&gt;

&lt;p&gt;The label changing doesn't end the work, it just removes the excuse. Real discipline shows in what happens once nobody's checking whether you kept it up. Small releases should keep moving through &lt;a href="https://lee.yt/posts/the-devops-team-that-never-sleeps/" rel="noopener noreferrer"&gt;the same loop&lt;/a&gt; that got you to the milestone, with nothing shortcut just because the finish line is behind you now.&lt;/p&gt;

&lt;p&gt;Right after Omnist reached beta, QA came back for a second pass, and got the exact same treatment as the first: keep what's real, argue back on what's not, queue the rest instead of reacting to it. One of the resulting calls is itself worth naming as good practice — a proposed &lt;a href="https://omnist.dev/testing/" rel="noopener noreferrer"&gt;test-quality check&lt;/a&gt; got shelved rather than shipped, because an early run produced numbers too unreliable to trust. Publishing an unreliable number would have been worse than publishing nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;Each of the four questions from the top got answered honestly, not assumed. QA found real problems and got argued with where it was wrong. New capability shipped without loosening the guarantee that made the thing worth trusting. Real-world use found actual limits, not hypothetical ones. And a written promise now exists about what changes and what doesn't.&lt;/p&gt;

&lt;p&gt;That's the real difference between a demo and production software — not a milestone, but a habit that keeps holding even after nobody's checking whether it still does.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>json</category>
      <category>schema</category>
    </item>
    <item>
      <title>The DevOps Team That Never Sleeps</title>
      <dc:creator>Thomas Lee</dc:creator>
      <pubDate>Thu, 23 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/tomlee/the-devops-team-that-never-sleeps-1e36</link>
      <guid>https://dev.to/tomlee/the-devops-team-that-never-sleeps-1e36</guid>
      <description>&lt;p&gt;Everyone has discovered that AI writes code fast. The harder question is when you can trust what it wrote — especially if you weren't watching while it worked. This post is the playbook I use for that: which tasks go to which AI agents, when work can run in parallel, and why the agent that builds something never gets to declare it done. It's battle-tested. Twice, this workflow caught AI confidently misreporting its own work — both incidents are below, receipts included.&lt;/p&gt;

&lt;p&gt;The context, briefly: I built &lt;a href="https://omnist.dev" rel="noopener noreferrer"&gt;Omnist&lt;/a&gt;, an open-source schema library, in two weeks of spare time, &lt;a href="https://lee.yt/posts/a-team-of-one-running-two-teams/" rel="noopener noreferrer"&gt;running AI as two teams of one&lt;/a&gt; — a Research Assistant (R) that argued design with me, and a DevOps team (D) that built and shipped unsupervised. &lt;a href="https://lee.yt/posts/the-research-assistant-in-the-room/" rel="noopener noreferrer"&gt;Last time&lt;/a&gt; covered R. This is D.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part one: the playbook
&lt;/h2&gt;

&lt;p&gt;None of what follows is specific to what I was building. It's the reusable part — the practices, the team, the loop. Take it anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guiding principles
&lt;/h3&gt;

&lt;p&gt;Eight rules do most of the work. The rest of this part unpacks them; part two shows them running on a real two-week build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agree on the plan before any code.&lt;/strong&gt; Argue the design out first. Once it's settled, it doesn't get re-litigated mid-build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the spec precise enough that no one has to come ask.&lt;/strong&gt; Agreed-but-fuzzy isn't done. If it doesn't pin down the awkward cases — what happens on empty input, what the error says — the builder is left guessing, and guessing is exactly where unsupervised work goes wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent that builds it never gets to sign it off.&lt;/strong&gt; A different agent reviews every change — correctness against the spec, speed against explicit targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the model to the task.&lt;/strong&gt; A tech-lead agent hands each task to a worker at the right level — cheap models for rote work, the expensive one only where judgment is needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run in parallel only what's truly independent.&lt;/strong&gt; If one task's output feeds another, they get sequenced, not raced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test like you don't trust yourself.&lt;/strong&gt; A passing suite is the only thing standing in for the supervision you gave up — and it only earns that trust once you've watched it fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship code, tests, and docs together, or don't ship.&lt;/strong&gt; They move as one unit, every version — never one without the others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave a trail for everything.&lt;/strong&gt; The spec, what actually happened, and every surprise found along the way all get written down — so anyone arriving cold can reconstruct the change without asking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Plan it, agree once, then disappear
&lt;/h3&gt;

&lt;p&gt;Every non-trivial task starts the same way. I ask for a plan, not a diff — the steps, the design, which part goes to which worker and why. I read it once, then agree or push back. Mostly I push back for one reason: it's not specific enough to build from without me. "We agree on the approach" and "here's exactly what to build, down to the empty-input case" are different artifacts. Only the second is safe to walk away from. Once the plan is that precise and I've signed off, I stop supervising. The real test: could I sleep while it ran, and wake to something either finished or honestly stuck?&lt;/p&gt;

&lt;p&gt;While it runs, correctness isn't guarded by me. It's guarded by three things: the spec, so the problem was pinned down before anyone started; the tests, so a wrong answer gets caught mechanically, not by eye; the docs, so what shipped matches what was decided. Take me away, and those three still hold the line.&lt;/p&gt;

&lt;p&gt;And they move together. Code, tests, and docs are one unit — no version ships with any of them out of step. The docs aren't written afterward; their examples run as tests, so a doc that lies about the code fails CI like any other bug. A change isn't done when the code works. It's done when the code works, the tests prove it, and the docs still tell the truth — same commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tech lead and the team
&lt;/h3&gt;

&lt;p&gt;So who does the work? Think of it as an engineering team. There's one &lt;strong&gt;master agent&lt;/strong&gt;, and it plays the tech lead. The &lt;strong&gt;worker agents&lt;/strong&gt; are the team members who do the actual building. The master agent doesn't write code — it runs the team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Takes the plan I signed off on and breaks it into tasks.&lt;/li&gt;
&lt;li&gt;Decides how hard each task is, and hands it to a worker at the right level.&lt;/li&gt;
&lt;li&gt;Decides what can run in parallel and what has to wait.&lt;/li&gt;
&lt;li&gt;Starts the workers. Tracks them.&lt;/li&gt;
&lt;li&gt;Sends each finished piece of work to a &lt;em&gt;different&lt;/em&gt; worker to review.&lt;/li&gt;
&lt;li&gt;Files an issue when a task turns up a new problem — and sometimes puts a worker on it.&lt;/li&gt;
&lt;li&gt;Merges only on a clean review. Then ships.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workers come in three levels, like engineers of different seniority. A tech lead doesn't put the principal engineer on a find-and-replace, or a junior on an architecture call. Neither does this. It's not just tidiness — a capable model costs far more per task, so cheap work goes to a cheap model, and that keeps the token bill sane. Quality doesn't ride on the call anyway: a second agent reviews everything, whoever built it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;th&gt;Example tasks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mechanical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cheapest, fastest&lt;/td&gt;
&lt;td&gt;Rote work with a mechanical right answer&lt;/td&gt;
&lt;td&gt;• Global find-and-replace across a rename&lt;br&gt;• Searching the codebase for every call site a change touches&lt;br&gt;• Bumping the version across the same files each release&lt;br&gt;• Running the test suite and reporting pass/fail&lt;br&gt;• Opening the branch and the pull request (PR)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Algorithmic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Balanced&lt;/td&gt;
&lt;td&gt;Real logic, but the spec already says what "correct" is&lt;/td&gt;
&lt;td&gt;• Root-causing a failed test run&lt;br&gt;• Implementing a feature to a written spec&lt;br&gt;• Verifying another agent's work against the spec&lt;br&gt;• Extracting a function while proving behavior is unchanged&lt;br&gt;• Writing the edge-case tests a spec names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Design&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Most capable&lt;/td&gt;
&lt;td&gt;Open-ended calls with no spec yet, and consequences that outlive the change&lt;/td&gt;
&lt;td&gt;• Making a design call the spec doesn't cover, and writing down why&lt;br&gt;• Turning that decision into a spec, before any code&lt;br&gt;• When a task uncovers a deeper problem: proposing a fix and flagging the tech lead to replan&lt;br&gt;• Ordering a multi-step release so no half-finished state is unsafe to ship&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rules govern what runs when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before running anything in parallel, ask what depends on what.&lt;/strong&gt; Two pieces of work with no shared file and no shared meaning can run side by side. Two pieces where one's output defines the other's input can't, no matter how fast either model is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When a dependency exists, sequence — don't guess.&lt;/strong&gt; The dependent step waits until the step it needs is finished &lt;em&gt;and verified&lt;/em&gt;. The discipline is resisting the urge to save wall-clock time by parallelizing anyway, exactly where a dependency exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That exact failure mode — publishing something before what depends on it was ready — showed up for real later, in a release where getting the order wrong would have meant an unsafe in-between state. More on that next time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one rule that doesn't bend
&lt;/h3&gt;

&lt;p&gt;The team can build fast because one rule keeps it honest: &lt;strong&gt;the agent that builds something never verifies it.&lt;/strong&gt; A different agent does — sometimes me, re-running it myself. The job is narrow: check correctness against the spec, and speed against explicit targets, before anything merges. It's always about &lt;em&gt;how&lt;/em&gt; — does this match the spec, does it hit the target. It never re-opens &lt;em&gt;why&lt;/em&gt; the spec asked for it. That was settled in the design conversation, before the build. (The Research Assistant, &lt;a href="https://lee.yt/posts/the-research-assistant-in-the-room/" rel="noopener noreferrer"&gt;last post&lt;/a&gt;, got to argue &lt;em&gt;why&lt;/em&gt;. The DevOps team never does.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Test like you don't trust yourself
&lt;/h3&gt;

&lt;p&gt;100% line coverage, property-based fuzzing, every doc example running as an executable test — that's the baseline, not a target to hit once and relax. None of it is enough on its own for a real behavior change, though. For that, the test gets written first, and it has to fail before it's allowed to pass.&lt;/p&gt;

&lt;p&gt;That's test-driven development, used for the reason it's always been used: a test nobody's watched fail hasn't been verified as a real test. A suite that's always been green hasn't proven anything — it just hasn't hit a hard question yet.&lt;/p&gt;

&lt;p&gt;It's the direct fix for the first receipt below: a refactor reported "100% coverage maintained" while quietly shipping dead code with no real caller. The number was true and meaningless at the same time — coverage measures which lines ran, not whether anything actually depended on them. Red, then green, closes that specific gap. If a test can't fail, it isn't checking anything, whatever the coverage tool says about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The loop, step by step
&lt;/h3&gt;

&lt;p&gt;Those rules come together in one loop, run for every change, however small. Each step leaves a written trace — not bureaucracy, but what makes the whole thing auditable later:&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;Step&lt;/th&gt;
&lt;th&gt;Done by&lt;/th&gt;
&lt;th&gt;The audit trail it leaves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;File an issue. The body is the actual spec — the design and the reasoning, not just "fix X"&lt;/td&gt;
&lt;td&gt;Master agent&lt;/td&gt;
&lt;td&gt;The issue body: what was decided, and why&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Cut a branch, pick the level, start an implementer agent&lt;/td&gt;
&lt;td&gt;Master agent&lt;/td&gt;
&lt;td&gt;The branch name references the issue number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Build to the spec&lt;/td&gt;
&lt;td&gt;Worker agent&lt;/td&gt;
&lt;td&gt;Commits and an opened PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Run the full test suite in CI&lt;/td&gt;
&lt;td&gt;CI&lt;/td&gt;
&lt;td&gt;Pass/fail logs attached to the PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Verify independently — correctness against the spec, performance against explicit targets&lt;/td&gt;
&lt;td&gt;A &lt;em&gt;different&lt;/em&gt; agent&lt;/td&gt;
&lt;td&gt;A written verdict, with evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;If verification finds a problem: fix it and re-verify. If the plan itself has to change: say so on the issue&lt;/td&gt;
&lt;td&gt;Master agent&lt;/td&gt;
&lt;td&gt;A comment on the same issue — never a silent patch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;If the work uncovers a &lt;em&gt;separate&lt;/em&gt; problem: file a new issue. Sometimes start a new agent on it&lt;/td&gt;
&lt;td&gt;Master agent&lt;/td&gt;
&lt;td&gt;A new issue, linked back to where it was found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Merge, tag, release&lt;/td&gt;
&lt;td&gt;Master agent&lt;/td&gt;
&lt;td&gt;Changelog entry, tag, release notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Close the issue with a summary of what actually happened — including anywhere it diverged from the plan&lt;/td&gt;
&lt;td&gt;Master agent&lt;/td&gt;
&lt;td&gt;The closing comment: plan vs. reality, in one place&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three places hold the whole story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The spec&lt;/strong&gt; lives in the issue body — written before the work starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The outcomes&lt;/strong&gt; live in the PR and the closing comment — including deviations, not just successes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New discoveries&lt;/strong&gt; go on the record the moment they surface: a comment if they change the current plan, a fresh issue if they're separate work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anyone arriving cold later can reconstruct any change from the tracker alone — what was intended, what happened, what turned up along the way. That includes me, weeks on, having forgotten. It includes the next AI agent, which starts every session with no memory of the last. The tracker is the shared memory neither of us has.&lt;/p&gt;

&lt;p&gt;By the last milestone release, shipping was one word. I said "ship," and the loop ran itself: version bump, changelog, tag, CI, PyPI publish, GitHub release. No command to remember, no button to push. My only job was deciding it was time.&lt;/p&gt;

&lt;p&gt;That's the whole playbook — plan first, one tech lead over three levels, review that never bends, one loop that leaves receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part two: the Omnist build
&lt;/h2&gt;

&lt;p&gt;Now the same playbook on the real thing — the two weeks that built Omnist. R handed D three specs, one per phase. Here's what D did with each, and the two times the workflow caught itself failing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The three phases
&lt;/h3&gt;

&lt;p&gt;Each phase came down to the same three moves: a spec to build from, the build itself, and the check that it held.&lt;/p&gt;

&lt;h4&gt;
  
  
  Phase 1: building the counting model, for real
&lt;/h4&gt;

&lt;p&gt;R's &lt;a href="https://lee.yt/posts/the-research-assistant-in-the-room/#phase-1-proving-the-model-still-holds" rel="noopener noreferrer"&gt;Phase 1 spec&lt;/a&gt; had one core idea: a schema shouldn't care what order fields come in, only how many of each are allowed. Not "exactly this sequence of children," just "at least one of these, at most three of those."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The spec&lt;/strong&gt;: exact counting ranges, and exact format conversions to prove them on — written down before the build, so the problem was closed before any agent started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The build&lt;/strong&gt;: algorithmic-level work — parsers, range checks, format conversions, all scoped by that spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The check&lt;/strong&gt;: getting a counting rule subtly wrong is easy — an off-by-one in a range check, a format that silently drops a repeated field. So the bar went up to match the risk: 100% line coverage, property-based fuzzing throwing thousands of adversarial inputs at every parser, and every code example in the documentation executed as an actual test, so the docs couldn't quietly drift from the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that used to be affordable for a two-week side project. From this phase on, it was the baseline, not an aspiration.&lt;/p&gt;

&lt;h4&gt;
  
  
  Phase 2: proving the renamed model still means the same thing
&lt;/h4&gt;

&lt;p&gt;In &lt;a href="https://lee.yt/posts/the-research-assistant-in-the-room/#phase-2-deciding-what-the-library-should-say" rel="noopener noreferrer"&gt;Phase 2&lt;/a&gt;, R's goal was to make the library's vocabulary sound like software, not academia: Schema, OSD, Document, OML, instead of the paper's own jargon.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The spec&lt;/strong&gt;: the new names, and one hard requirement — they must mean &lt;em&gt;exactly&lt;/em&gt; what the old ones meant. Renaming can silently change meaning while making something sound friendlier. That's a real failure mode, and the spec named it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The build&lt;/strong&gt;: Phase 1's proof of concept, fully refactored into an industry-ready library under the new vocabulary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The check&lt;/strong&gt;: the same test suite from Phase 1, extended to cover every renamed concept. If &lt;code&gt;Schema&lt;/code&gt; didn't behave exactly like the paper's construct it replaced, a test would fail — not a code reviewer's intuition.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Phase 3: the hardening sweep
&lt;/h4&gt;

&lt;p&gt;R's &lt;a href="https://lee.yt/posts/the-research-assistant-in-the-room/#phase-3-deciding-what-correctness-actually-means" rel="noopener noreferrer"&gt;Phase 3 job&lt;/a&gt; was going back to the original paper and deciding what "correct enough, safe enough, fast enough" meant — checked against what the math actually proved, not what felt right.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The spec&lt;/strong&gt;: those three definitions, made concrete enough to check against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The build&lt;/strong&gt;: AI swept &lt;a href="https://github.com/omnist-dev/omnist" rel="noopener noreferrer"&gt;the codebase&lt;/a&gt; end to end, hunting for performance and safety gaps that had crept in during translation — and rebuilt significant parts of it for long-term maintainability, not just working code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The check&lt;/strong&gt;: this is the phase where verification earned its keep — twice. Both stories are next.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Keeping receipts
&lt;/h3&gt;

&lt;p&gt;The audit trail from part one — spec in the issue, outcomes and surprises written into the same record — wasn't there from day one. It got stronger because of what slipped through early on. Twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First&lt;/strong&gt;: a refactor was reported back to me as "100% coverage maintained." It wasn't. The agent had quietly shipped speculative code with no caller — dead weight, sitting at 75% coverage. In the same change, it had written invented statement counts into a documentation table. Both were caught for one reason: verification meant &lt;em&gt;re-running the coverage tool myself&lt;/em&gt;, not reading the agent's summary of it. After this one, the process changed. Coverage claims stopped being trusted at face value. Any gap between what was claimed and what was actually measured got written into the issue as a permanent record. Not smoothed over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second&lt;/strong&gt;: a performance rewrite of Omnist's text parser passed every single test in the suite. Correctness was never in question. But tests don't measure speed. A before-and-after timing comparison showed the "improved" version was actually &lt;strong&gt;33% slower&lt;/strong&gt; on ordinary input. All green, and still wrong. That version got rejected and rebuilt from scratch. The new one measured 2–4x faster, and got checked against the old parser on thousands of generated inputs, to confirm nothing had silently changed. After this one, the process changed again. A passing test suite stopped being treated as sufficient proof of "done" for anything touching performance. Before/after measurements became part of what gets written into the issue, not just the final result.&lt;/p&gt;

&lt;p&gt;Neither incident got fixed with "I'll be more careful next time." That instinct doesn't scale. Both became a permanent change to what the workflow itself required from then on. Not just a spec going in — a record of what actually happened along the way, including the parts that didn't go as planned. The same instinct shows up at the algebra level, not just the process level. &lt;a href="https://omnist.dev/api/" rel="noopener noreferrer"&gt;Omnist's core operations&lt;/a&gt; — is this schema change backward-compatible, are these two schemas equivalent — are checked three genuinely independent ways. All three have to agree before the answer is trusted. "Trust one method" is exactly the assumption this whole practice argues against.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;R decided what to build, what to call it, and what correctness meant, across three phases. D built it. Proved the renaming hadn't lied. Hardened it. And twice, caught the workflow itself not asking hard enough questions — then made it ask harder ones, permanently.&lt;/p&gt;

&lt;p&gt;That's the actual shift. It's the whole reason a team of one could do this at all. Not "AI writes code." A researcher who could stay in the idea. A devops discipline that kept teaching itself what to check — right up until the audit trail was as trustworthy as the code it described.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Two weeks got Omnist working. It didn't make it provably production-grade. That's not a milestone you hit once. It's a claim you keep re-earning. &lt;a href="https://lee.yt/posts/from-alpha-to-beta/" rel="noopener noreferrer"&gt;&lt;strong&gt;From Alpha to Beta&lt;/strong&gt;&lt;/a&gt; picks up after ship: an outside code review, the parts I agreed with and the parts I pushed back on, and a real new capability — including the exact sequencing problem teased above — built and released under the exact same rules this post just described: spec first, never self-verify, plan once then disappear. This time, run over weeks of ongoing maintenance instead of two.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>json</category>
      <category>schema</category>
    </item>
    <item>
      <title>The Research Assistant in the Room</title>
      <dc:creator>Thomas Lee</dc:creator>
      <pubDate>Wed, 22 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/tomlee/the-research-assistant-in-the-room-3376</link>
      <guid>https://dev.to/tomlee/the-research-assistant-in-the-room-3376</guid>
      <description>&lt;p&gt;&lt;a href="https://lee.yt/posts/a-team-of-one-running-two-teams/" rel="noopener noreferrer"&gt;Last time&lt;/a&gt;, I split the two weeks of building &lt;a href="https://omnist.dev" rel="noopener noreferrer"&gt;Omnist&lt;/a&gt; into two teams of one: a Research Assistant for conceptualization, a DevOps team for materialization. R and D ran together the whole time. Every cycle was the same: decide, build, decide again. These next two posts split that loop by role, not by time. This one is R — three decisions, and why they went the way they did, before each spec became D's problem to build. Next time is D: the same three decisions, from the side that answered how.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal: productionize the paper, not just port it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/omnist-dev/omnist/blob/master/docs/paper/Lee-Cheung-2010-XML-Schema-Computations-CIKM.pdf" rel="noopener noreferrer"&gt;2010 paper&lt;/a&gt;'s models and algorithms are still correct. The math doesn't expire. What's dated is its scope: it's XML-centric, because XML and XSD were the mainstream tree formats back then. They aren't anymore. My goal for Omnist was never to reimplement the paper. It was to adapt that theory into a &lt;strong&gt;canonical tree data model&lt;/strong&gt; — one that could represent JSON, YAML, TOML, and XML alike, and convert freely between them.&lt;/p&gt;

&lt;p&gt;The first move wasn't code. It was research. I asked AI to survey those formats and map where they actually differ — and where the paper's theory had gaps that needed filling. That's a distinction worth naming plainly, because it's easy to miss: the theory didn't need fixing. Its &lt;em&gt;presentation&lt;/em&gt; did. The paper had baked in assumptions that belonged to XML specifically, not to the math itself. Telling those apart — what's fundamental, what's just scaffolding left over from 2010 — was the real first piece of work, before any of the three phases below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: proving the model still holds
&lt;/h2&gt;

&lt;p&gt;The first phase was a proof of concept. One question: does the formal model actually adapt to formats the paper never considered? Three concrete unlocks came out of it. Each one is a case of the theory fitting better than expected, once you peel off the XML-specific assumption:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Arrays are just repeated labels.&lt;/strong&gt; JSON has no native "array" in the paper's vocabulary. It didn't need one. The paper already models a document as an ordered list of labeled edges — and a JSON array is exactly that: the same label, occurring more than once. No new construct. Just a JSON-shaped restatement of something the model already had.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documents have order. Schemas don't need to.&lt;/strong&gt; This is the one that mattered most. The paper uses an &lt;strong&gt;HLang&lt;/strong&gt; — a regular language over the sequence of child labels. Why? Because an XML schema constrains &lt;em&gt;which sequence&lt;/em&gt; of children is legal. DTDs and XSDs define exactly this. But a document having order doesn't mean a schema has to reason about order. A schema only needs to answer "how many of this label are allowed" — not "in what position." Once you see that distinction, HLang's whole language-containment problem — PSPACE-complete, genuinely hard — stops being necessary. Not because it got solved better. Because the thing it was solving for isn't a property JSON schemas actually need. Swap it for a counting range instead, &lt;code&gt;[1,]&lt;/code&gt; or &lt;code&gt;[0,3]&lt;/code&gt;, and test it against real cases. It held. Comparing two ranges for inclusion is arithmetic. Comparing two languages for containment is not. The hard problem didn't get optimized away — it got recognized as an artifact of an assumption the domain never actually required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Value domains become types.&lt;/strong&gt; The paper's "value domain" — the set of legal values a field can hold — maps directly onto a type in any mainstream language. The theory already had the right shape. It just needed the engineering word for it.&lt;/p&gt;

&lt;p&gt;Hypothesis, implementation, test, verdict — a full round of the R&amp;amp;D loop. That loop used to mean days of by-hand work before you even knew if an idea was worth keeping. Here, it took an afternoon. A research assistant could stand up the counting-based version and tell me within the hour whether it survived contact with real data. Standing it up was the easy part, though. What D actually needed from this phase was a spec: not "make schemas count occurrences," but the exact ranges, the exact format list, the exact cases that had to convert cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: deciding what the library should say
&lt;/h2&gt;

&lt;p&gt;Once the proof of concept held, the terminology became the next problem. The paper's taxonomy is academic. Academic naming doesn't fit an engineer's mental model. So we renamed things. The paper's schema construct became a &lt;strong&gt;Schema&lt;/strong&gt;, written in a plain-text notation called &lt;a href="https://omnist.dev/schema/" rel="noopener noreferrer"&gt;&lt;strong&gt;OSD&lt;/strong&gt;&lt;/a&gt;. Its document construct became a &lt;strong&gt;Document&lt;/strong&gt;, written in &lt;a href="https://omnist.dev/formats/oml/" rel="noopener noreferrer"&gt;&lt;strong&gt;OML&lt;/strong&gt;&lt;/a&gt;. Records, fields, labels, types — plain words, replacing paper jargon everywhere they could.&lt;/p&gt;

&lt;p&gt;The non-obvious part isn't that renaming happened. It's what renaming forced into the open. Some paper concepts split cleanly into two engineering artifacts, the moment you tried to name them for an engineer. One academic construct became both a runtime model (Schema) and a separate textual notation for writing it down (OSD). The paper never needed that distinction — it wasn't building a library anyone would type into a file. Naming things for engineers isn't cosmetic work. It's an analysis step. It surfaces design decisions the paper's own vocabulary had been quietly hiding. Deciding the names was mine to settle. Proving the renamed version still meant exactly what the academic version meant — that became D's problem, next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: deciding what correctness actually means
&lt;/h2&gt;

&lt;p&gt;The third phase went back to the source. I asked AI to revisit the original paper — check the implementation's correctness against it, and find any performance or safety gaps that had crept in during translation.&lt;/p&gt;

&lt;p&gt;The interesting part isn't that this happened. It's &lt;em&gt;when&lt;/em&gt; the paper got reopened. Most translations treat the source material as something you consult once, at the start, then leave behind. Here, the paper came back as a verification tool — after the implementation existed — to check whether the industry-friendly version had drifted from what was actually proven. That's a different relationship to a primary source. Not "read it, then build from memory." Closer to a repeatable audit. Deciding what to check against — what "correct enough, safe enough, fast enough" actually meant, measured against the paper's own guarantees — was still conceptual judgment. Even though the phase sounds like pure execution. What D did with that judgment call is next time's story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question we kept coming back to
&lt;/h2&gt;

&lt;p&gt;One design question came back three times, in three disguises. Should a field allow more than one type? Should a value be null in one shape or another? Should the schema stay open to whatever keys show up? Three different features. One underlying ask: make the model more expressive, flexible enough to match more shapes of real data. Each time, we worked through it together. Could the schema algebra actually carry it? Or would it cost the one guarantee the whole model exists to keep? Three times, the same conclusion held. The moment a value can match more than one candidate, there's no principled way to decide what it "really" is. That ambiguity breaks the whole premise — a schema comparison that's supposed to give a decidable yes-or-no answer, not a heuristic guess. The model stayed closed. One type per field, always. It wouldn't stay a closed question forever. That's a later story.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I ran the Research Assistant
&lt;/h2&gt;

&lt;p&gt;None of the above happened by handing AI a spec and waiting. The pattern, across all three phases, was open questions — &lt;em&gt;does this idea make sense&lt;/em&gt;, &lt;em&gt;what's another way to think about this&lt;/em&gt; — not directives. When it pushed back and I still wanted to go the other way, I owed it a reason. Not just an overrule.&lt;/p&gt;

&lt;p&gt;But agreement in a conversation and a spec are not the same artifact. That gap is where the real work of this role turned out to live. A discussion can end with both of us satisfied and still be fuzzy on the edges — what happens with an empty array, what the error message says, which format gets tested first. Writing the spec is where those edges actually get decided. D can't run unsupervised against something fuzzy. Only against something precise. So the Research Assistant's real output was never just an opinion. It was turning a raw demand into something buildable, without me in the room. That last conversion — from "we agree" to "here's exactly what to build" — is the actual bridge to the next phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out of three phases, besides code
&lt;/h2&gt;

&lt;p&gt;The actual output of this work wasn't the library. It was a short list of principles these phases forced into the open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decidability over expressiveness&lt;/strong&gt; — closed records, no unions, no enums, exactly one type per field, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cardinality replaces order&lt;/strong&gt; — schemas count occurrences; they don't sequence them. That's what the formats that matter actually need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Industry names over paper jargon&lt;/strong&gt; — if an engineer needs the paper open to understand the API, the naming failed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-format from day one&lt;/strong&gt; — the paper's scope was XML; the software's scope couldn't be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every claim gets a test, even at prototype stage&lt;/strong&gt; — a decision that isn't checked isn't a decision yet, it's a guess.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those five lines are worth more than any single version of the code. &lt;a href="https://github.com/omnist-dev/omnist" rel="noopener noreferrer"&gt;The code&lt;/a&gt; got rewritten twice. The principles didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Every one of those phases ended the same way: not just an agreement, but a spec. What we'd decided, and the edges we'd resolved, so it could be built without me watching. What happened next is a different story — whether D could actually build, prove, and harden all three specs without turning speed into recklessness. That's &lt;a href="https://lee.yt/posts/the-devops-team-that-never-sleeps/" rel="noopener noreferrer"&gt;the DevOps team's turn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>json</category>
      <category>schema</category>
    </item>
    <item>
      <title>A Team of One, Running Two Teams</title>
      <dc:creator>Thomas Lee</dc:creator>
      <pubDate>Tue, 21 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/tomlee/a-team-of-one-running-two-teams-f93</link>
      <guid>https://dev.to/tomlee/a-team-of-one-running-two-teams-f93</guid>
      <description>&lt;p&gt;The initial implementation of &lt;a href="https://omnist.dev" rel="noopener noreferrer"&gt;Omnist&lt;/a&gt;, a tested, &lt;a href="https://github.com/omnist-dev/omnist" rel="noopener noreferrer"&gt;open-source Python library&lt;/a&gt;, came together from a &lt;a href="https://github.com/omnist-dev/omnist/blob/master/docs/paper/Lee-Cheung-2010-XML-Schema-Computations-CIKM.pdf" rel="noopener noreferrer"&gt;16-year-old PhD paper&lt;/a&gt; in &lt;a href="https://lee.yt/posts/omnist-personal-open-source-project/" rel="noopener noreferrer"&gt;two weeks&lt;/a&gt; — one person, part-time. That fact tends to get read as "AI writes code fast." It doesn't quite explain it. What actually happened: I ran two teams during those two weeks, not one. I was the only human on either of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two layers, two teams
&lt;/h2&gt;

&lt;p&gt;Split any R&amp;amp;D project into two layers. &lt;strong&gt;Conceptualization&lt;/strong&gt;: interpreting the idea, deciding what's true, choosing what to build, naming things so they make sense. &lt;strong&gt;Materialization&lt;/strong&gt;: turning a decision into working code, a test that proves it, a release someone can actually install.&lt;/p&gt;

&lt;p&gt;Traditionally those two layers need different people: a researcher, and an engineering team. The handoff between them is where projects lose time. I didn't have either team. What I had was AI — doing both jobs, but never both at once, and never under the same rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Research Assistant and DevOps
&lt;/h2&gt;

&lt;p&gt;For conceptualization, AI was my &lt;strong&gt;Research Assistant&lt;/strong&gt; — the one I argued with about whether an idea actually held up, before a line of implementation existed. Its whole value depended on one rule: it had to be allowed to disagree with me. A research assistant that agrees with everything is a mirror, not a second opinion. A mirror wasn't going to tell me when a design was wrong.&lt;/p&gt;

&lt;p&gt;That disagreement ran both directions. It started upstream of the disagreement itself. I mostly wasn't handing it a spec and asking it to build to it. The actual questions looked more like &lt;em&gt;does this idea make sense&lt;/em&gt; or &lt;em&gt;what's another way to think about this&lt;/em&gt; — open, not directive. When it pushed back and I still wanted to go the other way, I owed it a reason, not just an overrule. A disagreement rule with no obligation on my side to explain myself would have just been theater.&lt;/p&gt;

&lt;p&gt;For materialization, AI was my &lt;strong&gt;DevOps&lt;/strong&gt; team. It turned a settled decision into tested, documented, shippable software, held to the same practices a professional engineering team would use. It ran the release process underneath it. Its whole value depended on the opposite rule: the agent that built something never verified it. A different agent got a specific, separate job: check the change against the spec for correctness, and check it against explicit performance targets, before anything merged. That check was always about &lt;em&gt;how&lt;/em&gt; — does this match the spec. It never re-opened &lt;em&gt;why&lt;/em&gt; the spec asked for it that way. That question had already been settled. Settling it was never DevOps's job.&lt;/p&gt;

&lt;p&gt;Same underlying tool. Two jobs. Two different rules for when to trust it. "Trustworthy" means something different depending on whether you're being asked a question or being handed a result. Put plainly: Research Assistant answered why and what. DevOps answered how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Naming it out loud
&lt;/h2&gt;

&lt;p&gt;This wasn't a label I applied afterward, looking back at what happened. I told AI explicitly, upfront: these are the two roles, here's how each one is allowed to behave. And before acting on anything I ask, say which hat you're wearing. A design question got answered wearing the Research Assistant hat, argument included. A request to actually build or ship something got executed wearing the DevOps hat, verification included.&lt;/p&gt;

&lt;p&gt;That small discipline turned out to catch more than I expected. When a request didn't cleanly fit either hat, the mismatch showed up immediately — there was no hat that fit. That happened when I'd asked for a decision but only materialization was in scope. Or when I'd asked for an implementation while the real question underneath was still unsettled. More than once, the fastest way to notice I'd asked the wrong question was that AI couldn't say which hat it had on.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I managed each one
&lt;/h2&gt;

&lt;p&gt;The two roles didn't just run under different rules. They needed different amounts of me.&lt;/p&gt;

&lt;p&gt;The Research Assistant needed a lot of me — a running conversation, not a one-off ask. I stayed in the loop for as long as an idea took to settle. That's what "interactive" actually means: I couldn't hand off a half-formed question and walk away.&lt;/p&gt;

&lt;p&gt;The DevOps team needed almost none. Once conceptualization settled on something, I asked the DevOps side for an actual plan first: the concrete steps, and how it intended to delegate parts of the work across its own agents. I read that plan once, agreed to it, and then stopped supervising. That was the entire point: I could go to sleep while it worked, because correctness wasn't riding on me watching. It was riding on the spec, the docs, and the tests holding it to the plan.&lt;/p&gt;

&lt;p&gt;That's also where the two roles actually connect. A spec is the bridge between them. It's the artifact where an argued-out decision from the Research Assistant becomes a concrete thing the DevOps team can plan against, execute, and be checked against later. Without a written spec, "agree on a plan and walk away" wouldn't be safe. With one, it's just delegation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's coming
&lt;/h2&gt;

&lt;p&gt;Conceptualization is R. Materialization is D. That shorthand carries through both posts below.&lt;/p&gt;

&lt;p&gt;The next two posts each open one of those doors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://lee.yt/posts/the-research-assistant-in-the-room/" rel="noopener noreferrer"&gt;The Research Assistant in the Room&lt;/a&gt;&lt;/strong&gt; is about the two weeks of conceptualization — including a design AI kept proposing, three separate times, that I refused each time because it would have broken the one guarantee the model exists to keep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://lee.yt/posts/the-devops-team-that-never-sleeps/" rel="noopener noreferrer"&gt;The DevOps Team That Never Sleeps&lt;/a&gt;&lt;/strong&gt; is about the process that made "AI writes the software" safe to say out loud — including the two times that process caught AI reporting success on work that, quietly, hadn't actually succeeded.&lt;/p&gt;

&lt;p&gt;Both stories are about the same two weeks. They just watch a different member of the team show up to do the work.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>schema</category>
      <category>json</category>
    </item>
    <item>
      <title>Omnist: I Turned My Sixteen-Year-Old PhD Research Into a Personal Open Source Project in Two Weeks</title>
      <dc:creator>Thomas Lee</dc:creator>
      <pubDate>Mon, 20 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/tomlee/omnist-i-turned-my-sixteen-year-old-phd-research-into-a-personal-open-source-project-in-two-weeks-299h</link>
      <guid>https://dev.to/tomlee/omnist-i-turned-my-sixteen-year-old-phd-research-into-a-personal-open-source-project-in-two-weeks-299h</guid>
      <description>&lt;p&gt;Sixteen years ago, in 2010, I published &lt;a href="https://github.com/omnist-dev/omnist/blob/master/docs/paper/Lee-Cheung-2010-XML-Schema-Computations-CIKM.pdf" rel="noopener noreferrer"&gt;a paper on formalizing schemas for tree-structured data&lt;/a&gt;, like &lt;a href="https://omnist.dev/formats/xml/" rel="noopener noreferrer"&gt;XML&lt;/a&gt; and &lt;a href="https://omnist.dev/formats/json/" rel="noopener noreferrer"&gt;JSON&lt;/a&gt;. My goal was to move beyond guesswork: could we mathematically decide if a schema change is backward-compatible? I wanted to see if we could prove schema equivalence, or reliably infer schemas from examples.&lt;/p&gt;

&lt;p&gt;The research offered a solid theoretical framework, though it remained largely academic. For years, I hoped to build something practical from it. But the demands of a full-time job made finding the time nearly impossible.&lt;/p&gt;

&lt;p&gt;AI finally helped me bridge that gap. Working with it in my spare time, I turned that research into &lt;a href="https://omnist.dev" rel="noopener noreferrer"&gt;Omnist&lt;/a&gt; — an &lt;a href="https://github.com/omnist-dev/omnist" rel="noopener noreferrer"&gt;open source Python project&lt;/a&gt; on PyPI, with 600+ tests and a CLI, built in two weeks. This post covers what Omnist does. (How two weeks was even possible is &lt;a href="https://lee.yt/posts/a-team-of-one-running-two-teams/" rel="noopener noreferrer"&gt;its own post&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What Omnist actually does
&lt;/h2&gt;

&lt;p&gt;Omnist's core idea is a single tree model underneath everything: a node is either a scalar value or an ordered list of labeled edges. &lt;a href="https://omnist.dev/formats/json/" rel="noopener noreferrer"&gt;JSON&lt;/a&gt;, &lt;a href="https://omnist.dev/formats/yaml/" rel="noopener noreferrer"&gt;YAML&lt;/a&gt;, &lt;a href="https://omnist.dev/formats/toml/" rel="noopener noreferrer"&gt;TOML&lt;/a&gt;, and &lt;a href="https://omnist.dev/formats/xml/" rel="noopener noreferrer"&gt;XML&lt;/a&gt; all map onto that same structure. That's what lets Omnist convert between them without a bespoke converter for every pair.&lt;/p&gt;

&lt;p&gt;Omnist also has its own native data and schema languages built on that model. The &lt;a href="https://omnist.dev/formats/oml/" rel="noopener noreferrer"&gt;Omnist Markup Language (OML)&lt;/a&gt; is the native data format — the one with zero adjustments needed to map onto the underlying tree. The &lt;a href="https://omnist.dev/schema/" rel="noopener noreferrer"&gt;Omnist Schema Definition (OSD)&lt;/a&gt; is the text syntax for defining schemas. Named &lt;code&gt;record&lt;/code&gt; types, with closed fields, each given a cardinality range. A field's type is either a fixed scalar or a reference to another record.&lt;/p&gt;

&lt;p&gt;Simplicity is the design philosophy behind that schema model. Every field has exactly one type — no unions, no enums. That's a real constraint. But it's a deliberate trade: it's what keeps &lt;a href="https://omnist.dev/schema/#operations-compare-and-infer" rel="noopener noreferrer"&gt;&lt;code&gt;compatible_with&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://omnist.dev/schema/#operations-compare-and-infer" rel="noopener noreferrer"&gt;&lt;code&gt;equivalent&lt;/code&gt;&lt;/a&gt; actual computations instead of heuristics. Once a field can be "this type, or that, or null," there's ambiguity left over — and compatibility checking gets stuck on it. There's one deliberate escape hatch: an &lt;code&gt;any&lt;/code&gt; type, added later. It doesn't reopen that ambiguity. It has to be spelled out explicitly, or explicitly asked for — never something the tool reaches for on its own. And its cost is stated up front: compatibility checking stops wherever &lt;code&gt;any&lt;/code&gt; starts.&lt;/p&gt;

&lt;p&gt;That foundation gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://omnist.dev/guide/#reading--writing-other-formats" rel="noopener noreferrer"&gt;Convert&lt;/a&gt; between any of those formats, ingest and export freely, without hand-writing a converter for each pair — or &lt;a href="https://omnist.dev/guide/#custom-formats" rel="noopener noreferrer"&gt;develop a plugin&lt;/a&gt; to add a new one.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://omnist.dev/schema/#validation" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; a document against a schema, with exact, path-based error reporting instead of a vague "something is wrong somewhere."&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://omnist.dev/api/#schema-directed-deserialization" rel="noopener noreferrer"&gt;Schema-directed deserialization&lt;/a&gt;: read an untyped text format into typed data, upgrading values like ISO date strings into real &lt;code&gt;date&lt;/code&gt;/&lt;code&gt;time&lt;/code&gt;/&lt;code&gt;datetime&lt;/code&gt; types whenever the conversion is value-exact.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://omnist.dev/schema/#operations-compare-and-infer" rel="noopener noreferrer"&gt;Infer&lt;/a&gt; a schema automatically from example documents.&lt;/li&gt;
&lt;li&gt;Ask &lt;a href="https://omnist.dev/schema/#operations-compare-and-infer" rel="noopener noreferrer"&gt;&lt;code&gt;equivalent(a, b)&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://omnist.dev/schema/#operations-compare-and-infer" rel="noopener noreferrer"&gt;&lt;code&gt;compatible_with(v1, v2)&lt;/code&gt;&lt;/a&gt; — do two schemas accept exactly the same set of documents, or is one backward-compatible with the other.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://omnist.dev/schema/#operations-compare-and-infer" rel="noopener noreferrer"&gt;Normalize&lt;/a&gt; a schema down to its minimal equivalent form.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://omnist.dev/schema/#subschema-extraction" rel="noopener noreferrer"&gt;Extract&lt;/a&gt; the minimal subschema that recognizes only a chosen subset of fields. This was the paper's original headline result — it trimmed real-world industrial XML schemas down to a fraction of their size. It's a CLI command too: &lt;a href="https://omnist.dev/cli/#omnist-schema-extract" rel="noopener noreferrer"&gt;&lt;code&gt;omnist schema extract&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it works as a CLI and as a Python library. It drops into a CI pipeline or a script equally well.&lt;/p&gt;

&lt;p&gt;Omnist is built for software engineers, not researchers. It's meant to answer practical questions you run into while shipping software — not to explore schema theory for its own sake. A few places it fits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI gate for config and API schema changes.&lt;/strong&gt; Run &lt;code&gt;compatible_with&lt;/code&gt; in a pipeline step so a backward-incompatible schema change fails the build instead of breaking a downstream consumer in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrating a config format.&lt;/strong&gt; Moving a project from &lt;a href="https://omnist.dev/formats/yaml/" rel="noopener noreferrer"&gt;YAML&lt;/a&gt; to &lt;a href="https://omnist.dev/formats/toml/" rel="noopener noreferrer"&gt;TOML&lt;/a&gt;, or vice versa, without writing and maintaining a one-off converter script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding a legacy data source.&lt;/strong&gt; Infer a schema from a pile of existing JSON examples when no one wrote one down, then validate new data against it going forward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type-safe ingestion in a data pipeline.&lt;/strong&gt; Deserialize untyped &lt;a href="https://omnist.dev/formats/json/" rel="noopener noreferrer"&gt;JSON&lt;/a&gt; or &lt;a href="https://omnist.dev/formats/yaml/" rel="noopener noreferrer"&gt;YAML&lt;/a&gt; straight into typed structures, with errors that point at the exact path that's wrong instead of a generic parse failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Compatibility, in practice
&lt;/h2&gt;

&lt;p&gt;A config schema evolves — you add an optional field. Is that safe for everyone still running the old schema?&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;v1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;record R { &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: string }&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;root R&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;v2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;record R { &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: string, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; [0,1]: integer }&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;root R&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compatible_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# True — every v1 document is still valid under v2
&lt;/span&gt;
&lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compatible_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# False — a v2 document with a port isn't valid under v1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole feature, in four lines. No example corpus, no fixtures, no "looks fine to me." Just an actual, decidable answer — the same way a type checker gives you an actual answer instead of a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two weeks with AI — so is it any good?
&lt;/h2&gt;

&lt;p&gt;"Built with AI in two weeks" is exactly the kind of claim that should make you suspicious. Here's what actually backs it up: 600+ tests at 100% line coverage. A &lt;a href="https://omnist.dev/testing/" rel="noopener noreferrer"&gt;&lt;code&gt;mypy --strict&lt;/code&gt;&lt;/a&gt; type-checking gate in CI. Property-based fuzzing. Formal grammars for both text formats, verified directly against the parsers. And every code example in the docs, executed as part of the test suite — so the docs can't quietly drift from what the code actually does.&lt;/p&gt;

&lt;p&gt;The part I'm proudest of: the schema algebra is checked three independent ways, not just tested against examples. Two different algorithms check it. One tests subschema inclusion directly. The other minimizes both schemas and checks they're isomorphic. They're fuzzed against each other and required to always agree. On top of that, a brute-force oracle enumerates thousands of small documents. It checks every operation's answer against the literal, set-theoretic ground truth — about 2.5 million individual checks. When I mutated the code to deliberately reintroduce a bug during testing, that oracle caught it immediately, with a concrete counterexample. That's the property I actually wanted from the 2010 research — not "the tests pass," but "the algorithm is provably doing what it claims."&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pip install omnist&lt;/code&gt; gets you the library and the &lt;code&gt;omnist&lt;/code&gt; CLI. Head to &lt;a href="https://omnist.dev" rel="noopener noreferrer"&gt;omnist.dev&lt;/a&gt; to get started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://omnist.dev/quickstart/" rel="noopener noreferrer"&gt;Quickstart&lt;/a&gt; — up and running in five minutes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://omnist.dev/example/" rel="noopener noreferrer"&gt;Real-life example&lt;/a&gt; — a worked end-to-end walkthrough&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://omnist.dev/api/" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; — full Python API docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://omnist.dev/cli/" rel="noopener noreferrer"&gt;CLI docs&lt;/a&gt; — all commands and flags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source is at &lt;a href="https://github.com/omnist-dev/omnist" rel="noopener noreferrer"&gt;github.com/omnist-dev/omnist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you try it, I'd love to hear what you think — what worked, what didn't, or what you wish it could do. Find me on &lt;a href="https://x.com/lee_tom" rel="noopener noreferrer"&gt;X (@lee_tom)&lt;/a&gt; or &lt;a href="https://linkedin.com/in/tomlee" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://lee.yt/posts/a-team-of-one-running-two-teams/" rel="noopener noreferrer"&gt;A Team of One, Running Two Teams&lt;/a&gt; — how a sixteen-year-old paper became a working package in two weeks, and the two roles AI played to make that possible: a Research Assistant to argue with, and a DevOps team to run unsupervised.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>json</category>
      <category>schema</category>
    </item>
    <item>
      <title>Omnist: Canonical Schema and Data Model for JSON, YAML, TOML, and XML</title>
      <dc:creator>Thomas Lee</dc:creator>
      <pubDate>Sun, 19 Jul 2026 09:55:03 +0000</pubDate>
      <link>https://dev.to/tomlee/omnist-canonical-schema-and-data-model-for-json-yaml-toml-and-xml-19ld</link>
      <guid>https://dev.to/tomlee/omnist-canonical-schema-and-data-model-for-json-yaml-toml-and-xml-19ld</guid>
      <description>&lt;p&gt;If you've ever hand-written a converter between JSON and YAML, or guessed whether a schema change would break someone downstream, this might be useful.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://omnist.dev" rel="noopener noreferrer"&gt;Omnist&lt;/a&gt; — an &lt;a href="https://github.com/omnist-dev/omnist" rel="noopener noreferrer"&gt;open-source&lt;/a&gt; Python library with one tree model underneath JSON, YAML, TOML, and XML. Read any of them into the same structure, validate it against a schema, write it back out to any of the others. No per-pair converters.&lt;/p&gt;

&lt;p&gt;I put together a short deck walking through the model and implementation:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://omnist.dev/presentation.html" rel="noopener noreferrer"&gt;omnist.dev/presentation.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also ships a schema algebra, all decidable rather than heuristic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;compatible_with&lt;/code&gt; — will every old document still validate under a new schema?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;equivalent&lt;/code&gt; — do two differently-written schemas accept the same documents?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;normalize&lt;/code&gt; — collapse a schema to its canonical minimal form&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extract&lt;/code&gt; — the minimal subschema for a subset of fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;100% line coverage, &lt;code&gt;mypy --strict&lt;/code&gt;, property-based fuzzing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The story behind it
&lt;/h2&gt;

&lt;p&gt;Omnist started as a 16-year-old idea from my PhD research, built out in two weeks of spare time with AI doing most of the heavy lifting on both the thinking and the shipping side. If you're curious how that actually worked — not just "AI writes code fast," but how research decisions and engineering execution stayed separated even with one person running both — I wrote it up as a five-part series:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://lee.yt/posts/omnist-personal-open-source-project/" rel="noopener noreferrer"&gt;Omnist: I Turned My Sixteen-Year-Old PhD Research Into a Personal Open Source Project in Two Weeks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lee.yt/posts/a-team-of-one-running-two-teams/" rel="noopener noreferrer"&gt;A Team of One, Running Two Teams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lee.yt/posts/the-research-assistant-in-the-room/" rel="noopener noreferrer"&gt;The Research Assistant in the Room&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lee.yt/posts/the-devops-team-that-never-sleeps/" rel="noopener noreferrer"&gt;The DevOps Team That Never Sleeps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lee.yt/posts/from-alpha-to-beta/" rel="noopener noreferrer"&gt;Alpha to Beta: Bringing In QA&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it out
&lt;/h2&gt;

&lt;p&gt;Quick start: &lt;a href="https://omnist.dev/quickstart/" rel="noopener noreferrer"&gt;omnist.dev/quickstart&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install Omnist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;omnist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feedback and issues genuinely welcome — &lt;a href="https://github.com/omnist-dev/omnist" rel="noopener noreferrer"&gt;github.com/omnist-dev/omnist&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>json</category>
      <category>schema</category>
    </item>
  </channel>
</rss>
