Aditya Gaur
Work

Native macOS app

Loam

A markdown app for the agent era with no AI inside it, built around a press that produces byte-identical HTML on any machine.

Role
Solo — product, design, engineering
Timeframe
2026 — ongoing
Status
in development
Stack
Swift 6·SwiftUI·WebKit·swift-markdown·JavaScriptCore

The file your agent just wrote

You close a coding session. Somewhere in the repo there is now a PLAN.md you didn't write, a handoff note, a research report, three new files under .claude/skills/, and a CLAUDE.md that has quietly grown to eleven thousand tokens.

You need to read it. Not skim it — read it, because it's the artifact you're about to act on.

So you cat it in the terminal and it comes out as a wall of asterisks. You open the VS Code preview, which renders it in a pane the width of a phone. You push it and read it on GitHub, which is nicer, and now the thing you wanted to check is behind a commit you made in order to check it.

Every one of those tools assumes a human wrote the file and a human will read it once. That was true for twenty years. It stopped being true about two years ago.

Markdown became the substrate of the agent era without anyone deciding it should. CLAUDE.md, AGENTS.md, SKILL.md, plan files, changelogs, handoff docs, research reports. Agents read and write it constantly, and the apps built to hold it — Typora, iA Writer, Nota — were all designed for one person writing one document. The newer AI-native editors bolt an assistant on the side, and are web-stack apps with none of the Mac-native craft.

Loam is my answer to that. A reader good enough to be the system default for .md, and a press that turns markdown into HTML the same way every single time.

The app has no AI in it

This is the decision everything else follows from, so it goes first.

Loam makes no model calls. Not a chat pane, not a rewrite command, not a summarise button. There is no network entitlement in the build. You can pull the cable and nothing degrades. No account, no telemetry.

The reasoning is narrower than "AI bad." Loam's job is to be the surface you check an agent's work with. A reviewing instrument that also guesses is not a reviewing instrument. If the app can rewrite my document, then every time something reads strangely I have a second suspect, and I have to rule out my own tool before I can trust what I'm reading. That's a bad trade for a feature I can get from the terminal in the next window.

So the agents stay outside and the app stays predictable. Which turns out to be a much harder promise than it sounds, because "predictable" has a precise definition and almost nothing meets it.

What byte-identical actually means

Loam's press makes one promise: the same document bytes, the same PressVersion, and the same theme version produce byte-identical HTML on any machine, at any time.

Output is a pure function of exactly three things: the document, the engine's output-format version, and the theme's version. Everything else is forbidden. LoamEngine has no code path that reads the clock, the locale, randomness, the environment, the network, or any filesystem state that isn't the input document.

That list is worth operating rather than reading. Below is the contract as a working model: change one of the three permitted inputs and the digest moves, which is correct. Switch on one of the six forbidden reads and watch which gate catches it.

Plate 01the determinism contract
The three permitted inputs

Output is a pure function of exactly these. Change one and the bytes are supposed to change.

PressVersion
Theme version
The six forbidden reads

The engine has no code path to any of these. Switch one on to see what it would cost.

What the runners produced
Render 1macos-15 · Swift 6.0 · JSC 19df245ad962b87384held
Render 2same machine, immediately afterdf245ad962b87384held
Render 3macos-26 · Swift 6.2 · JSC 22df245ad962b87384held

df245ad962b87384 — one cell per hex digit

passmake verify — double render (same machine, rendered twice, bytes compared)Both renders on macos-15 produced the same bytes.
passmake verify — golden compare (output vs the checked-in golden file)Output matches the recorded bytes.
passDeterminism workflow — cross-generation (every fixture × every theme, two runner generations, SHA-256 manifests compared)macos-15 and macos-26 produced identical manifests.
Golden file df245ad962b87384

Recorded bytes and current output agree. Nothing to do.

Plate 01. The contract, and the three gates that enforce it. Switch on a forbidden read to break the build.

Real PressVersion and theme versions. The digest is FNV-1a standing in for SHA-256 — it models “the bytes changed”, not Loam's actual hashing.

The distinction the lab makes is the one I got wrong when I started. I assumed nondeterminism was one problem. It's two, and they need different gates.

The clock and the RNG move between two renders on the same machine, so rendering twice and comparing catches them immediately. The locale, the environment, the filesystem and the network are perfectly stable within one machine and differ across machines. Render twice locally and everything passes, then it breaks on someone else's laptop six weeks later. Only rendering on genuinely different machines catches that class. That's why the CI gate renders every fixture on two macOS runner generations with different Xcode, Swift and JavaScriptCore builds, takes a SHA-256 manifest of every output file from each, and asserts the manifests match.

Different OS, different compiler, different JS engine, same bytes. Or the badge goes red.

The engine can't cheat because it can't reach

The contract is enforced by architecture before it's enforced by tests.

LoamEngine takes bytes and returns bytes. It performs no file I/O at all. Every read and write lives in the CLI layer or the app layer, which pass content in and write results out. The engine cannot consult the disk because the engine has no way to reach the disk. That's a stronger guarantee than a rule in a style guide, and it's the reason the same code can back the CLI, the app, and the Quick Look extension without three renderers drifting apart.

The parser is swift-markdown, pinned. The serializer is mine. Walking the AST to HTML myself is more work than using an off-the-shelf renderer, and it's the only way the output bytes are actually under my control. Extensions like callouts and wiki-links need to slot into the same serializer, and a third-party renderer's escaping choices become my escaping choices whether I like them or not.

Two pieces of JavaScript run at press time, and they're held to the same rule as everything else. A vendored KaTeX 0.16.22 turns TeX into MathML, and a vendored highlight.js 11.11.1 marks up code. Both evaluate inside their own JSContext on their own JSVirtualMachine — no DOM, no clock, no locale, no network in the context — and both emit static markup, never scripts. hljs.highlight is a pure function of the code and the language, so pinned source plus pinned input means pinned output.

Plate 02press stages

Split the document

YAML frontmatter is lifted off the top and parsed into a metadata card. The remaining bytes are what the parser sees. Nothing here consults a schema registry or a file on disk — the document carries its own metadata.

In
---
title: Determinism
---

# Determinism

Same bytes in.
Out
meta: { title: "Determinism" }
body: "# Determinism\n\nSame…"

Plate 02. One document through the press, stage by stage. Every stage is a pure function of what the stage before it handed over.

4,650
lines of Swift
3
themes, each separately versioned
2
macOS generations in the byte-compare gate
0
network calls in the engine

What the promise costs

Determinism is not free, and the bill arrives in small, irritating instalments.

No timestamps. Every generator puts "generated at" in a comment. Loam can't, because a timestamp is the clock, and the clock is forbidden. People notice its absence and read it as an oversight.

Sorting looks wrong. Ordering has to be fixed binary order, because locale-aware collation is the locale. Binary order puts Z before a. A human reading that list sees a bug. It is the contract working.

A dependency bump becomes a product decision. Upgrading the vendored KaTeX changes output bytes, which means it's an output change like any other: bump the version, regenerate the recorded golden files in the same commit, review the golden diff. There is no such thing as a quiet patch upgrade.

The gate is loud by design. Any deliberate CSS tweak fails CI until the goldens are re-recorded. That friction is the point. A golden mismatch with no version bump is always either accidental nondeterminism or an unacknowledged output change, and I want to be stopped in both cases. But it does mean the honest workflow has an extra step, every time, forever.

I'd take that trade again. What it buys is that a rendered-page diff in a pull request contains your content change and nothing else, which makes reviewing generated HTML possible rather than theatre. And when a page renders oddly, it renders identically oddly everywhere, so the bug reproduces on the first try.

What exists, and what is still a document

I've been describing a product brief and a working program, and it matters which is which.

Plate 03build state

Reader and press, both real

This is what 4,650 lines of Swift currently do. The engine is shared by the app, the CLI, and the Quick Look extension, so there is one renderer and one set of bytes.

  • 01The reader — a typeset page with an outline rail that unfolds on approach, word count and reading time, and light, dark, and high-contrast sharing one typographic system
  • 02Live file watching — when an agent or a git checkout rewrites the open file, the page updates in place and keeps your scroll position
  • 03The press — one document to a self-contained HTML file, or a folder to a small static site with generated navigation and incremental rewrites
  • 04Three themes — article, docs, and minimal, each versioned independently as part of the contract
  • 05The determinism suite — double-render, golden byte-compare, a real-document corpus, and the cross-generation CI gate
  • 06The CLI — the same engine, headless, so a build pipeline needs no app installed

Plate 03. An honest inventory. The reader and the press run today; the seam and the editor are specified and unbuilt.

The reader shipped first on purpose. It's the smallest complete thing that's useful on its own, it's the part you touch every day, and it forced the typography and the engine into existence, both of which everything else needs. The press came second because it's the same engine pointed at a file instead of a window.

The editor is last because it's the one that can fail. Hybrid editing that hides marks as the caret leaves them, while round-tripping the author's original markdown byte-for-byte, is genuinely hard, and it's the component where I could spend eight weeks and end up with something merely acceptable.

What I'd do differently

I hedged on the editor instead of deciding. The brief gives it a four-week spike with an explicit fallback to a web-based editor if the native version misses the quality bar. That looked like risk management when I wrote it. Reading it back, it's a way of not choosing — and it's the reason the editor is still the part I haven't started. A fallback you've already written down is a decision you've already half-made.

Three themes was one too many. Each is separately versioned, so each is a separate golden set and a separate thing to keep honest under the gate. Two would have taught me the same things about the theme system for two thirds of the maintenance.

I should have written the determinism contract down first. It exists as a document now, and having it in prose is what made the four enforcement layers obvious. They fall straight out of the sentence "output is a pure function of exactly three inputs." I built roughly half of the enforcement before I wrote that sentence, and I built the wrong half first: I had the golden compare, which catches drift, before I had the cross-machine gate, which catches the failures I'd actually have shipped.