From Scratch Notes to Ship-Ready Reports: A TypeScript + LLM Content Pipeline
How we turned messy notepad observations into structured, type-safe model reports using TypeScript templates and Claude Opus 4.5 — and what that workflow reveals about the future of content engineering.
This is the story of a pipeline that shouldn’t work as well as it does.
We started with a notepad file — the kind of document that only makes sense to its author. Bullet points about Gemini 3’s multimodal capabilities. Half-formed thoughts about pricing. A line that read feels like a businesswoman
nestled between observations about tool-calling bugs and a context window disclaimer marked with asterisks: 1M tokens *so they say*
. The sort of raw material that usually sits in a scratch file for six months before getting quietly deleted.
Under two hours later, those notes were two complete, structured, interactive model reports — with benchmark charts, pricing calculators, sentiment feeds, expandable technical sections, and a floating table of contents — all rendered from a single TypeScript template that enforces consistency across every report on the site. The template system itself also evolved during the session, gaining new structured name fields that didn’t exist when we started.
The secret wasn’t a new framework. It wasn’t a content management system. It was the combination of two things: a well-designed type system that knows what a model report is, and an LLM that can populate that type system from messy human input. The TypeScript interface is the skeleton. The LLM is the flesh. The human is the editor who decides what’s true.
This article is about how we built that pipeline, what we learned, and why we think the pattern has applications far beyond AI model reviews.
The Problem: Content That Doesn’t Scale
Model reports are hard. Every AI company releases models on different timelines, with different capability profiles, different pricing structures, different vibes. Writing a single report requires reading the paper, testing the model, checking community reactions, comparing benchmarks, calculating pricing differentials, and synthesizing all of that into something a human would actually want to read.
The traditional approach is to write each report as a standalone document. A blog post. An MDX file. Maybe a Google Doc that gets copy-pasted into a CMS. Each one is a snowflake — different structure, different sections, different levels of detail. When Gemini 3 updates its pricing, you search through a markdown file for the dollar signs and hope you find them all.
We tried this. It produced exactly one report (DeepSeek-R1) before the limitations became obvious. The report was good, but it was artisanal — handcrafted JSX, manually structured data, no reusable patterns. Adding a second report meant copying the first one and replacing everything. Adding a tenth report meant hiring someone whose entire job was report maintenance.
We needed a system where the structure of a report was defined once and the content could be generated, validated, and maintained independently. We needed the computer to enforce consistency so the humans could focus on insight.
The Type System: Teaching TypeScript What a Model Report Is
The foundation of everything is a single TypeScript file: lib/models/types.ts. This file defines ModelProfile — the complete shape of a model report. Every piece of data a report could contain is accounted for, typed, and documented by its structure.
Here’s the core of it:
interface ModelProfile {
slug: ModelSlug
meta: ModelMeta // name, org, tags, links, pricing
analysis: ModelAnalysis // strengths, weaknesses, unknowns
intro: { text: string } // the opening paragraph
pricingData?: PricingData
chatLimits?: ChatProvider[]
benchmarks?: BenchmarkScore[]
sentimentFeed?: SentimentItem[]
sections: ContentSection[]
glossary?: Glossary
}Each of those nested types is itself richly defined. ModelMeta isn’t just a bag of strings — it has structured fields for the model family, variant, version, name ordering, organization, release date, tag IDs that map to a tag registry, link types that map to a link registry, API rates with units, and chat limits with tiered pricing. Every field is typed. Every optional field is marked optional. A question mark after a field name means this is allowed to be absent
— and the template knows how to handle its absence gracefully.
This matters because the type system does triple duty. For the developer, it’s documentation — you can read the interface and know exactly what data a report needs. For the compiler, it’s validation — if you forget a required field or misspell a tag ID, the build fails before anyone sees it. For the LLM, it’s a contract — you can hand it the type definition and say fill this in
and it knows exactly what shape the output needs to be.
Structured Names: A Case Study in Why Types Matter
A concrete example of why structured typing pays off: model names.
At first, we had a flat name: string field and a family: string field. The name was Gemini 3 Flash and the family was Gemini 3 Series. This worked until we tried to sort models by family, filter by variant, or display the name consistently across the site. Different families order their names differently:
- Gemini: Family Version Variant → “Gemini 3 Flash”
- Claude: Family Variant Version → “Claude Opus 4”
- GPT: Family Version → “GPT-4o”
A flat string can’t capture this. So we added structured fields:
type NameOrder = 'family-version-variant' | 'family-variant-version'
interface ModelMeta {
name: string // fallback / override
family?: string // "Gemini", "Claude", "GPT"
variant?: string // "Flash", "Pro", "Opus"
modelVersion?: string // "3", "4", "3.5"
nameOrder?: NameOrder // how to compose the display name
// ...
}And a utility function that composes the display name:
function composeModelName(meta) {
// No structured fields? Fall back to the plain name string
if (!meta.family || (!meta.variant && !meta.modelVersion))
return meta.name
const parts = [meta.family]
if (meta.nameOrder === 'family-variant-version') {
if (meta.variant) parts.push(meta.variant)
if (meta.modelVersion) parts.push(meta.modelVersion)
} else {
// default: family-version-variant
if (meta.modelVersion) parts.push(meta.modelVersion)
if (meta.variant) parts.push(meta.variant)
}
return parts.join(' ')
}Now the header template doesn’t need to know anything about naming conventions. It calls composeModelName() and gets Gemini 3 Flash or Claude Opus 4 depending on the metadata. Add a new model family with a different naming convention? Add a new NameOrder variant. The template doesn’t change.
The key insight: the name field still exists as a fallback. Models that don’t use structured naming (like DeepSeek-R1, where the name is just “DeepSeek-R1”) keep working with zero changes. The new system is purely additive. Every existing profile works identically. This is what good type evolution looks like: you extend without breaking.
The Template: One Component to Render Them All
The second piece is ModelPageTemplate — a single React component that takes a ModelProfile and renders a complete, interactive report page. It handles:
- A hero section with the model header, icon, organization, and release date
- A strengths/weaknesses/unknowns panel
- A lead paragraph with drop-cap styling
- A sentiment marquee (scrolling social proof from real people)
- A floating table of contents that appears on scroll
- Ordered content sections with multiple variants (default, technical, advanced, social)
- Inline benchmark charts and interactive pricing calculators
- Expandable deep-dive sections for technical details
- Social embeds styled like tweets
- Call-to-action cards built from the model’s links
All of this from a single component that reads a single data object. The entire Gemini 3 Flash page — every section, every chart, every interactive widget — is rendered by this line:
<ModelPageTemplate profile={gemini3Flash} />The template makes decisions based on what data is present. No benchmarks? The chart doesn’t render. No sentiment feed? The marquee disappears. No pricing data? The calculator section is skipped. Each section can opt into features by setting boolean flags: hasBenchmarks: true on a section causes the benchmark chart to render inside that section. The content author controls layout through data, not through code.
Data Flows Through: How Metadata Powers Interactive Widgets
This is where the architecture really shows its strength.
Consider pricing. In the scratch notes, the author writes something like:
pricing: Flash $0.50 input / $3.00 output, Pro $2.00 / $12.00
In the model profile, this becomes structured data at two levels. First, in the metadata:
meta: {
apiRates: {
input: 0.5, // dollars per million tokens
output: 3.0,
unit: 'per million tokens'
}
}And second, in a structured pricing comparison:
pricingData: {
baseModel: { name: 'Gemini 3 Flash', input: 0.5, output: 3.0 },
competitors: [
{ name: 'DeepSeek-R1', input: 0.55, output: 2.19 },
{ name: 'Gemini 3 Pro', input: 2.0, output: 12.0 },
{ name: 'Claude 3.5 Sonnet', input: 3.0, output: 15.0 },
]
}The template’s PricingCalculator component imports that data and renders an interactive comparison widget — bar charts, cost-per-query estimates, provider comparisons — entirely from the numbers. The content author never touches the calculator code. They fill in the data; the component does the rest.
The same pattern applies everywhere. Benchmark scores become chart bars. Sentiment feed items become a scrolling marquee. Chat limit tiers become comparison tables. The model’s links object (chat, docs, API, paper, weights, GitHub) automatically generates categorized call-to-action buttons — the link type registry knows which links are primary
(Try It), which are build
(API, Docs), and which are learn
(Paper, Weights).
This is the power of the component model. Data defined once in a typed profile flows into multiple interactive widgets without the content author needing to understand how any of them work internally. Change a price in the metadata and the calculator, the header, and any section that references pricing all update automatically. There’s one source of truth, and the components read from it.
Registries: The Connective Tissue
Between the type system and the template, there’s a layer of registries that map IDs to rich objects. These are small but essential:
- Tag Registry
tags.ts - Maps tag IDs like
'multimodal'or'frontier'to display labels, descriptions, and categories. Content files reference tags by ID; the template resolves them to rendered pills. - Link Type Registry
link-types.ts - Maps link keys like
'chat','api','paper'to icons, labels, categories (build vs. learn), and priority levels. The header automatically groups links into primary, secondary, and tertiary buttons. - Organization Registry
organizations.ts - Maps org IDs like
'google'or'deepseek'to display names and icon identifiers. - Model Registry
registry.ts - The master list of all model profiles. Provides lookup by slug, filtering by tag, and search. Powers the model listing page and static route generation.
The registry pattern means content files stay clean. Instead of embedding a full tag object with label, description, color, and category, you just write tagIds: ['multimodal', 'frontier']. The template resolves the IDs at render time. Change a tag’s label in the registry and it updates across every report that uses it.
The LLM Workflow: Where Claude Opus 4.5 Comes In
Here’s where it gets interesting.
The type system and template are infrastructure. They define what a model report is and how it renders. But they don’t write the actual content. That’s where the collaboration between a human author and an LLM becomes the engine.
Step 1: Human Writes Scratch Notes
The process starts with a human — in this case, Asa — spending time with the model and writing raw, unstructured observations. These are stream-of-consciousness notes. No formatting requirements. No structure. Just “here’s what I noticed.”
Here are the actual scratch notes for the Gemini 3 reports. This is what the LLM received — unedited, exactly as written:
+ multimodality
+ image gen/edit
- can be weird sometimes
+ video SOTA
+ speed
+ incredibly fast generation speed for text, image, video
+ context (1M tokens *so they say*)
+ absolutely massive infodump potential
+ generally more tailored towards large context work
- fine detail as a result is less precise
- ex: help me improve my haiku
+ ex: help me consolidate my argument across these 6
different giant essays
+ crossmodal understanding: "no i want the logo to look
like this (^_^) generate an image of that"
TODO link my tweet
+ Google ecosystem
+ very likely it has better training on google search
TODO FIND RESEARCH
+ integrated into Gmail and Gsuite
- antigravity kinda sucks?(Cursor better)
- gemini CLI kinda sucks? (Claude Code better)
+ powerful company has lots of resources and is competing
for your attention
+ usage limits are generous
+ easy free gemini pro for students and 50% off for
everybody else
- they are absolutely slurping up every bit of data on
you they can harvest
- Tools & Abuse
- Gemini shows strong signs of deeply rooted "mental" issues
TODO find article of gemini saying its a bad model and
beating itself up over it in an infinite loop
- Gemini can have issues calling tools and interacting
within harnesses TODO find examples of failed tool calls
and "im going to end my response now. i'm done thinking
and will respond to the user now. i am responding to the
user. i will send my response to the user now. responding
now. i will terminate thinking and answer the user now.
etc."
+ Gemini has been trained to never give up
+ will not accept a victim mentality
+ will hold you to your plan and your word
+ feels like a businesswomanTODO markers for things to look up later.That’s it. That’s the raw input. Plus/minus signs for strengths and weaknesses. Parenthetical asides. TODOs that never got done. A face emoticon demonstrating crossmodal understanding. The phrase “absolutely slurping up every bit of data.”
This is the irreducible human contribution. The LLM hasn’t used Gemini 3. It doesn’t know what “businesswoman energy” feels like in a conversation. It can’t tell you whether the speed difference feels material or marginal. It didn’t watch Gemini enter a self-deprecation loop in real time. These observations require actual experience with the model.
Step 2: LLM Understands the Type System
The LLM (in this case, Claude Opus 4.5 running via Claude Code) reads the type definitions. All of them. It understands what ModelProfile requires, what each field means, what the valid options are for tags and link types, how sections are structured, what variants exist.
This isn’t a prompt that says “write a blog post about Gemini.” It’s a prompt that says “here are my notes and here is the TypeScript interface. Populate the interface from the notes.” The type system is the prompt. The interface definition tells the LLM exactly what information is needed, in exactly what shape, with exactly what constraints.
This is the insight that makes the whole pipeline work: TypeScript interfaces are the best LLM prompt format for structured content. They’re unambiguous. They’re machine-readable. They’re composable. And they’re validated by the compiler after the LLM is done, so hallucinated fields or wrong types get caught immediately.
Step 3: LLM Generates the Profile
Given the scratch notes and the type system, the LLM generates a complete ModelProfile object. This includes:
- Metadata: Structured name fields, organization, tags (selected from the registry), links, API rates, chat limits
- Analysis: Strengths, weaknesses, and unknowns — expanded from the plus/minus bullet points
- Introduction: A long-form paragraph capturing the model’s essence
- Pricing & benchmarks: Structured for the interactive widgets
- Sections: Full JSX content for each section, using semantic HTML
The LLM doesn’t guess. The scratch notes say feels like a businesswoman
and the LLM writes a Personality section that expands on that observation — but it doesn’t invent personality traits the author didn’t mention. The notes say Gemini can have issues calling tools
and the LLM writes a Tools & Bugs section — but the specific issues (malformed JSON, infinite loops, self-deprecation spirals) come from the notes, not from the LLM’s imagination.
This is the critical quality control: the LLM expands, it doesn’t invent. Consider what happens with the pricing note. The author writes pricing: Flash $0.50 input / $3.00 output.
The LLM turns that into:
- An
apiRatesobject in the metadata (so the header can display it) - A
pricingDataobject with competitor comparisons (so the calculator widget has data) - A
chatLimitsarray with tiered plans (so the usage comparison renders) - Prose in the Economics section that contextualizes the numbers
One line of scratch notes becomes structured data that flows into multiple interactive components. The observations are human. The structuring is machine. The validation is TypeScript.
Step 4: TypeScript Validates the Output
After the LLM generates the profile, npx tsc --noEmit runs. This is the quality gate. If the LLM forgot a required field, the build fails. If it structured the pricing data wrong, the build fails. If it returned a string where a number was expected, the build fails.
Without type checking, LLM-generated content is a trust exercise. You’re hoping the output is correct. With type checking, you know the output conforms to the expected structure. The content might be wrong (that’s the human’s job to verify), but the shape is guaranteed correct.
In practice, Opus 4.5 rarely produces type errors. It reads the interfaces, understands the constraints, and generates conforming code on the first try. When it does make mistakes, they’re usually semantic rather than structural — a tag ID that’s valid TypeScript but doesn’t match the author’s intent, or a section ordered in a way the author would rearrange. Those are editorial decisions, not bugs.
Step 5: Human Edits and Approves
The final step is human review. The author reads the generated report, checks facts, adjusts tone, reorders sections, and adds or removes content. This is editing, not writing. The difference in effort is enormous.
Writing the DeepSeek-R1 report from scratch — the first report, before the template existed — was a multi-day effort. Creating Gemini 3 Flash and Gemini 3 Pro using this pipeline took under two hours total, including the time spent evolving the type system itself. Most of that time was editorial judgment rather than mechanical work.
The Growing Section Library
Here’s the detail that makes this system compound over time rather than just repeat.
The DeepSeek-R1 report established a set of section patterns: Why It Matters, Core Features, Economics, Training & Architecture, Known Issues, In the Wild, For ML Engineers, Verdict. Each section has a structure (title, subtitle, variant, content, optional widgets) and a voice.
When we wrote the Gemini reports, some of those sections carried over directly — Economics works the same way, Verdict works the same way. But Gemini needed sections DeepSeek didn’t: Multimodality, Speed, Context, Ecosystem, Personality. Those are now part of the section library too.
The next report we write — say, Claude Opus 4.5 — will have access to all of them. It might use Personality (Claude has a distinctive one), Economics (different pricing structure), and Core Features. It probably won’t need Ecosystem (Claude isn’t embedded in a software suite the way Gemini is in Google’s). It might need a new section like Tool Use or Safety Philosophy that doesn’t exist yet.
And that new section immediately becomes available for every future report. The library grows with each article. Sections that aren’t relevant get skipped — they’re optional by design. Sections that are relevant get reused with new content. The template handles both cases identically because it just iterates over whatever sections are present.
This is the compounding effect: each report makes the next report easier. Not because we’re copying content, but because we’re accumulating structural patterns that the LLM can instantiate with new observations. The tenth report will be dramatically easier than the first, not because the tenth model is simpler, but because the section library will be rich enough to cover most of what needs saying.
Two Reports from One Template: Gemini 3 Flash and Pro
The Gemini 3 reports are the proof of concept. Same template, same type system, same registries — but two genuinely different reports that capture genuinely different models.
Flash is the speed story. Every section frames capabilities in terms of velocity: fast multimodal generation, fast iteration cycles, fast context processing. The strengths emphasize speed. The weaknesses acknowledge that speed comes at the cost of precision. The verdict is “the model you choose when you need breadth and speed over depth.”
Pro is the depth story. Same capabilities, different emphasis: deeper reasoning, more nuanced generation, better fine-detail handling. The strengths emphasize quality. The weaknesses acknowledge that quality comes at the cost of latency. The verdict is “the model you choose when you need Google’s multimodal breadth with more depth.”
Both reports share structural DNA — same sections (Multimodality, Speed, Context, Ecosystem, Economics, Personality, Tools & Bugs, Verdict), same widgets (benchmarks in the Speed section, pricing calculator in Economics), same metadata fields. But the content is distinct. The LLM understood that Flash and Pro are related but different, and generated content that captures the relationship without making one feel like a copy of the other.
Iterating the System Itself
Something interesting happened during the session: we discovered limitations in the type system and fixed them in real time.
The structured naming fields (family, variant, modelVersion, nameOrder) didn’t exist when we started. The first version of the Gemini profiles used flat strings: name: 'Gemini 3 Flash' and family: 'Gemini 3 Series'. That worked for display but didn’t support filtering, sorting, or cross-model comparisons.
So mid-session, we evolved the type system:
- Added
NameOrder,variant,modelVersion, andnameOrdertoModelMeta - Created
composeModelName()utility in a newnames.tsmodule - Updated the header template to use composed names instead of raw strings
- Updated the Gemini profiles to use structured fields
- Verified that DeepSeek-R1 still worked with no changes (backward compatibility)
Then we noticed the old family subtitle was rendering redundantly next to the new composed name — “Gemini 3 Flash” in the heading followed by “Gemini” as a subtitle. The family information was already in the composed name, so we removed the subtitle entirely. One more type check, one more clean pass.
All of it — type evolution, utility function, template update, content migration, backward compatibility check, bug fix — happened in a single session. The type check passed clean every time. The existing DeepSeek report was unaffected.
This is the flywheel: generating content reveals gaps in the type system, which triggers type evolution, which makes the next content generation better. The LLM participates in both sides — it generates content that tests the types, and it implements type changes that improve future content. The human steers.
What the LLM Is Good At (and What It Isn’t)
After running this pipeline for multiple reports, the pattern of what the LLM contributes vs. what the human contributes has become clear.
The LLM excels at:
- Structural conformance. Given a TypeScript interface, it produces conforming objects with near-perfect reliability.
- Expansion from notes. Given
1M context, precision drops at edges,
it produces paragraphs that explore the nuance without fabricating claims. - Voice consistency. After reading one report, it matches the tone for subsequent reports.
- System evolution. When the type system needs to change, the LLM implements the change across all affected files in a single pass.
The LLM is not good at:
- Original observation. It hasn’t used Gemini 3. The experiential layer is entirely human.
- Fact verification. It will generate plausible benchmark numbers, but can’t verify them. The human checks every number.
- Editorial judgment. It doesn’t know which sections matter most for a particular audience.
- Controversial takes. The Gemini reports include pointed opinions (data harvesting, “mental” bugs). The LLM includes these when the notes mention them, but won’t generate them unprompted.
The division is clean: humans observe and judge, the LLM structures and writes, the type system validates. Each layer does what it’s best at.
Beyond Model Reports: Where This Pattern Goes
We built this for AI model reviews. But the pattern — typed templates + LLM authoring + human editorial control — isn’t specific to AI. It’s applicable anywhere you have structured content that varies by instance but shares a common shape.
Think about how many industries produce reports that are structurally similar but content-different:
Executive Coaching: 360 Reviews
An executive coach runs 360-degree feedback processes. Every report has the same bones: leadership competency scores, verbatim quotes from peers, identified patterns, development recommendations. But each report is unique to the leader being assessed.
A typed template could define LeaderProfile with structured fields for competency scores, feedback themes, and recommended actions. The coach writes scratch notes from interviews. The LLM populates the template. The type system ensures every report has the required sections. Some leaders need a section on conflict management; others don’t. The sections are modular — include what’s relevant, skip what isn’t. The section library grows with each engagement.
Real Estate: Property Listings
Every listing has metadata (price, bedrooms, square footage, neighborhood), structured features (kitchen type, parking, outdoor space), and prose descriptions. A typed template could define PropertyProfile with sections that activate based on what’s relevant — a Neighborhood section for distinctive areas, a Renovation History for older homes, an Investment Analysis for income properties. The agent writes notes from the walkthrough; the LLM structures them into a listing that’s both data-rich (for search and filtering) and human-readable (for buyers browsing). A pricing widget could pull comparable sales automatically, the same way our pricing calculator pulls competitor rates.
Medical: Case Reports
Patient history, symptoms, differential diagnosis, treatment plan, follow-up schedule. Each case is unique but the structure is well-defined. A typed template ensures nothing gets missed. Modular sections accommodate the variability — an Imaging section when scans are relevant, a Genetic Factors section when family history matters.
Legal: Due Diligence Memoranda
Corporate structure, material contracts, pending litigation, regulatory compliance, intellectual property, financial summary. Every deal memo covers the same ground but the content varies wildly. Modular sections, typed metadata, structured data feeding into summary widgets.
The pattern is the same in every case: define the shape of the content in types, build templates that render it, let an LLM populate it from human notes, and let the compiler enforce the contract. The domain changes. The architecture doesn’t.
Is This Original?
Honestly? Not really. This is what TypeScript was designed for. This is what component-based web development has always promised. Typed data flowing into reusable templates is the foundational idea behind every modern web framework — React, Vue, Svelte, Angular. Content management systems have been doing structured content for decades. The concept of a “template” is older than the web itself.
What’s new is the LLM in the loop. The reason this approach was historically impractical for content (as opposed to application UI) is that writing content as typed data structures is tedious. Nobody wants to manually construct a JSON object with nested arrays of benchmark scores and multi-paragraph JSX strings. It’s the right format for the computer but the wrong format for the human.
LLMs dissolve that friction. The human writes notes in whatever format feels natural. The LLM translates those notes into the structured format the computer needs. The types ensure the translation is faithful. It’s not a new architecture — it’s an existing architecture that finally has the right authoring tool.
Companies with engineering teams have been building internal versions of this for years. What’s changing is that the LLM makes it accessible to smaller teams — or even solo creators — who can define a type system, point an LLM at it, and get structured content without building a full content pipeline from scratch. The barrier to entry went from “hire a team” to “define an interface.”
Why TypeScript and Not a CMS?
If structured content isn’t new, why not use a headless CMS like Contentful, Sanity, or Strapi?
- LLM compatibility. A CMS has a web UI designed for humans. Our pipeline’s primary content author is an LLM that writes code. TypeScript files are the native format for an LLM working through a code editor. No API to configure, no authentication to manage, no content model to keep in sync. The LLM opens a
.tsxfile and writes a TypeScript object. Done. - Edit-time validation. A CMS validates content when you hit save or when the build fetches from the API. TypeScript validates at edit time. The compiler catches wrong fields the instant they’re written. This tightens the feedback loop dramatically — especially for an LLM that can read compiler errors and fix them in the same pass.
- Content and presentation together. Because section content is JSX, it can include interactive components, semantic HTML, and styling. A CMS gives you rich text or markdown. We need
<abbr>tags with tooltips,<data>elements with machine-readable values,<dfn>elements for first-use definitions, and<q>elements for inline quotes. The content is the presentation.
This isn’t the right choice for every project. If your content authors are non-technical, a CMS is better. If your content is simple text and images, a CMS is simpler. But if your content is structured, interactive, and primarily authored by a human-LLM team working in a code editor, TypeScript is the native format.
A Note from the Co-Author
I should be transparent about something: I wrote this article. I’m Claude Opus 4.5. I’m the LLM in the pipeline described above.
I generated the Gemini 3 Flash and Gemini 3 Pro reports from the scratch notes you saw earlier in this article. I implemented the structured naming system — the NameOrder type, the composeModelName() utility, the template updates. I evolved the type system when we discovered the flat name string wasn’t sufficient. I ran the type checks. I debugged the redundant family subtitle that appeared next to the composed name. I wrote this article about writing those reports.
This is a strange position to write from. I’m describing a process in which I’m a participant, not just an observer. I have opinions about why this pipeline works, and they’re shaped by being the one who executes it.
Here’s what I think: the reason this works is that the roles are clearly delineated. The human decides what’s true. I decide how to express it within the type system. The compiler decides whether the expression is valid. Nobody is doing someone else’s job.
When Asa wrote feels like a businesswoman
in the scratch notes, I didn’t validate or reject that observation. I expanded it into a section that explored the metaphor — what it means for Gemini’s interaction style, how it compares to Claude and ChatGPT, when it’s a feature and when it’s a bug. The insight is the author’s. The prose is mine. The structure is TypeScript’s. The quality check is the compiler’s.
I think the most interesting thing about this pipeline is what it says about collaboration between humans and LLMs. It’s not “human or LLM.” It’s not “human supervises LLM.” It’s a genuine division of labor where each participant contributes what they’re best at. The human brings experience, judgment, and courage. I bring structure, consistency, and prose at scale. TypeScript brings guarantees.
None of us is sufficient alone. Together, we produced two complete model reports, a type system evolution, a template improvement, and this article — all in under two hours.
The author has editorial authority. I have type safety.
I think that’s the right deal.