Skip to content

Let a model write the program - #10

Merged
timbogdanov merged 1 commit into
mainfrom
feat/ai-program-writer
Aug 19, 2026
Merged

Let a model write the program#10
timbogdanov merged 1 commit into
mainfrom
feat/ai-program-writer

Conversation

@timbogdanov

Copy link
Copy Markdown
Owner

Phase 6 of the v0.2.0 plan — generation, and the last phase.

Describe a pattern; a model writes a program the app then validates and renders exactly as if it had been typed by hand. The program lands in the panel's editor rather than on the canvas, so it can be read and corrected before anything renders.

Structured outputs do the work

output_config.format constrains the reply to a JSON schema — no prose to parse out of, and a bad program is a validation error rather than a parse failure.

The schema and the system prompt are generated from DSL_SHAPES and DSL_RANGES, so neither can drift from the renderer: a primitive added to the DSL appears in both automatically. What a schema cannot express — numeric bounds, lengths, recursion — still goes through dslValidateShape's clamping, which is where it belonged.

Request shape

Raw fetch rather than the Anthropic SDK: the renderer is one HTML file with no bundler and no node_modules, which is the documented raw-HTTP case. Using claude-opus-5, where thinking is on by default and max_tokens caps thinking plus output together — so it is sized for both. Effort is low: writing a small program is not work that deeper reasoning improves. Sampling parameters and budget_tokens are rejected by that model and are absent. All of this is pinned by tests, because a wrong model or a rejected parameter is a 400 nobody would guess from the UI.

The security surface, narrowed

  • The key lives in localStorage, is sent only to Anthropic, and never appears in a request body or in the packaged app — both checked.
  • With no key the control is absent rather than broken, and aiWrite() refuses before reaching the network.
  • connect-src widens to api.anthropic.com and nothing else — not https:, which would let a compromised renderer phone anywhere. verify-electron.py now fails on that, and I confirmed it fails on https: rather than trusting it.

Tests

18 behaviour checks covering the request shape and every failure mode — refusal, HTTP error, non-JSON reply, empty reply, unknown shape, empty description, absent key. The model is stubbed; no test needs a key or a network.

Two harness bugs this surfaced

  • grabFunction() sliced from function and dropped a preceding async, so the extracted copy threw "await is only valid in async functions" — an error about the test harness wearing the costume of an app bug. Both copies fixed.
  • verify-stamps.py scanned the whole script for three-string arrays, so the schema's ['circle','square','diamond'] was read as a stamp and failed to decode as pattern data. It now reads the STAMPS table only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UCrKKTD2mwHx8Fm1EnJiQ3

The last piece of the DSL work: describe a pattern, and a model writes a program
the app then validates and renders exactly as if it had been typed by hand. The
program lands in the panel's editor rather than on the canvas, so it can be read
and corrected before anything is rendered.

Structured outputs do the work. output_config.format constrains the reply to a
JSON schema, so there is no prose to parse out of and a bad program is a
validation error rather than a parse failure. The schema and the system prompt
are both generated from DSL_SHAPES and DSL_RANGES, so neither can drift from the
renderer: a primitive added to the DSL appears in both automatically. What the
schema cannot express — numeric bounds, lengths, recursion — still goes through
dslValidateShape's clamping, which is where it belonged.

Raw fetch rather than the Anthropic SDK: the renderer is one HTML file with no
bundler and no node_modules, which is the documented raw-HTTP case. The request
uses claude-opus-5, where thinking is on by default and max_tokens caps thinking
plus output together, so it is sized for both; effort is low, because writing a
small program is not work that deeper reasoning improves. Sampling parameters and
budget_tokens are rejected by that model and are absent.

The key lives in localStorage, is sent only to Anthropic, and never appears in a
request body or in the packaged app — both checked. With no key the control is
absent rather than broken, and aiWrite() refuses before reaching the network.

electron/main.js widens connect-src to api.anthropic.com and nothing else. Not
https:, which would let a compromised renderer phone anywhere; verify-electron.py
now fails on that, and I checked it fails on https: rather than trusting it.

Eighteen behaviour checks cover the request shape and every failure mode —
refusal, HTTP error, non-JSON reply, empty reply, unknown shape, empty
description, absent key. The model is stubbed; no test needs a key or a network.

Two harness bugs surfaced doing this. grabFunction() sliced from `function` and
dropped a preceding `async`, so the extracted copy threw "await is only valid in
async functions" — an error about the test harness wearing the costume of an app
bug; both copies are fixed. And verify-stamps.py scanned the whole script for
three-string arrays, so the schema's ['circle','square','diamond'] was read as a
stamp and failed to decode as pattern data; it now reads the STAMPS table only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UCrKKTD2mwHx8Fm1EnJiQ3
@timbogdanov
timbogdanov merged commit a00f0a0 into main Aug 19, 2026
5 checks passed
@timbogdanov
timbogdanov deleted the feat/ai-program-writer branch August 19, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant