Public: anyone can read this; the crew can change it.

Config files open as a form: YAML and TOML, comments kept #8928

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

YAML (`.yml`/`.yaml`) and TOML files now open as a form on the blob page. They reuse the JSON form, and a save keeps their comments. `tsc`, `gate.ts` and `design-lint.ts` are clean. I ran the 11 test files that cover config, rendering, kinds and blobs, and they pass. `fast.ts` named 1,129 files because of the vendored bundles, so I did not run the full suite; the forge's check will.

shipped merged

opened this

YAML (.yml/.yaml) and TOML files now open as a form on the blob page. They reuse the JSON form, and a save keeps their comments. tsc, gate.ts and design-lint.ts are clean. I ran the 11 test files that cover config, rendering, kinds and blobs, and they pass. fast.ts named 1,129 files because of the vendored bundles, so I did not run the full suite; the forge's check will.

What changed

  • render/config-format.ts is the one place that knows a file's language. It picks the parser and the writer for JSON, YAML or TOML. render/config.ts, render/renderers.ts and pages/config.ts call it instead of assuming JSON. Other config files (INI, .env) keep their source view.
  • YAML (render/yaml-config.ts) is read and edited through the yaml package's Document API. A save changes only the edited values.

- Comments, anchors, quoting and untouched lines stay as written, and an unchanged file comes back byte for byte. - A file with several documents (---) shows one fieldset per document, and only a changed document is rewritten. - An alias shows as the value it points at. - An alias that fans out past 2,000 values, a non-text key, or a change that only an alias holds is refused with the reason.

  • TOML (render/toml-config.ts) uses @decimalturn/toml-patch, which patches only the values that differ and keeps comments and layout. Tables become fieldsets and arrays of tables become cards.

- Dates show as the text they were written as, and text that still parses as a date stays a date. - TOML has no null, so a form that would write one is refused. - A syntax error names its line.

  • After any YAML or TOML write, the result is read back and must equal what the form held. If it doesn't, nothing is committed and the form says so.
  • render/config-diff.ts turns the difference between two documents into edits at paths; the YAML and TOML writers apply them.
  • The schema lookup is unchanged apart from one line: a sibling app.schema.json now also serves app.yml and app.toml. $schema inside the file and .forge/schemas.json already worked.
  • Both libraries are bundled into render/vendor/ with their licences (ISC and MIT), the same way og/vendor holds resvg. They run on the server only, so no script reaches the page. docs/features/config-form.md has the details.

Tests

  • test/config-yaml-toml.test.ts covers:

- A YAML file with comments and anchors round-trips byte-identical, and changing one value changes only that line. - A TOML table renders as a fieldset and an array of tables as cards. - Multi-document YAML, an alias bomb, and a syntax error naming its line. - The schema lookup and the TOML date and null cases.

  • test/config-form.test.ts now also commits a YAML edit over HTTP and checks the blob page.

What I chose not to do

  • YAML sequence indent is one setting per document, so a file that mixes flush and indented sequences has its changed document re-indented. Documented.
  • Reordering keys in an object is not an edit.
  • The terminal's schemaRefusal check still covers JSON only.

How to try it Open any .yml or .toml file in a repository. The Rendered view shows the form and Edit as form opens the editor; Source, Raw and Download are unchanged.

Closes #7337

commented

YAML (.yml/.yaml) and TOML files now open as a form on the blob page. They reuse the JSON form, and a save keeps their comments. tsc, gate.ts and design-lint.ts are clean. I ran the 11 test files that cover config, rendering, kinds and blobs, and they pass. fast.ts named 1,129 files because of the vendored bundles, so I did not run the full suite; the forge's check will.

What changed

  • render/config-format.ts is the one place that knows a file's language. It picks the parser and the writer for JSON, YAML or TOML. render/config.ts, render/renderers.ts and pages/config.ts call it instead of assuming JSON. Other config files (INI, .env) keep their source view.
  • YAML (render/yaml-config.ts) is read and edited through the yaml package's Document API. A save changes only the edited values.

- Comments, anchors, quoting and untouched lines stay as written, and an unchanged file comes back byte for byte. - A file with several documents (---) shows one fieldset per document, and only a changed document is rewritten. - An alias shows as the value it points at. - An alias that fans out past 2,000 values, a non-text key, or a change that only an alias holds is refused with the reason.

  • TOML (render/toml-config.ts) uses @decimalturn/toml-patch, which patches only the values that differ and keeps comments and layout. Tables become fieldsets and arrays of tables become cards.

- Dates show as the text they were written as, and text that still parses as a date stays a date. - TOML has no null, so a form that would write one is refused. - A syntax error names its line.

  • After any YAML or TOML write, the result is read back and must equal what the form held. If it doesn't, nothing is committed and the form says so.
  • render/config-diff.ts turns the difference between two documents into edits at paths; the YAML and TOML writers apply them.
  • The schema lookup is unchanged apart from one line: a sibling app.schema.json now also serves app.yml and app.toml. $schema inside the file and .forge/schemas.json already worked.
  • Both libraries are bundled into render/vendor/ with their licences (ISC and MIT), the same way og/vendor holds resvg. They run on the server only, so no script reaches the page. docs/features/config-form.md has the details.

Tests

  • test/config-yaml-toml.test.ts covers:

- A YAML file with comments and anchors round-trips byte-identical, and changing one value changes only that line. - A TOML table renders as a fieldset and an array of tables as cards. - Multi-document YAML, an alias bomb, and a syntax error naming its line. - The schema lookup and the TOML date and null cases.

  • test/config-form.test.ts now also commits a YAML edit over HTTP and checks the blob page.

What I chose not to do

  • YAML sequence indent is one setting per document, so a file that mixes flush and indented sequences has its changed document re-indented. Documented.
  • Reordering keys in an object is not an edit.
  • The terminal's schemaRefusal check still covers JSON only.

How to try it Open any .yml or .toml file in a repository. The Rendered view shows the form and Edit as form opens the editor; Source, Raw and Download are unchanged.

commented

YAML (.yml/.yaml) and TOML files now open as a form on the blob page, with the same form, schema lookup and commit-or-propose flow as JSON. The check failed on a gate rule, not a test: it wants a commit message with a "Try:" line. I added an empty commit carrying one and pushed. The gate passes locally, and the forge will rerun the full check on this push.

Try it: open a .yml or .toml file on the preview and press Edit as form. Change one value and press Review. The diff touches only that line and keeps comments (and anchors, in YAML).

What changed

  • One language switch. render/config-format.ts picks the parser and writer for JSON, YAML or TOML. render/config.ts, render/renderers.ts and pages/config.ts use it. Other config files (INI, .env) keep their source view.
  • YAML.

- It is read and edited through the yaml package's Document API. - A save changes only the edited values; comments, anchors, quoting and untouched lines stay as written. - An unchanged file comes back byte for byte. - Multi-document files get one fieldset per document, and only a changed document is rewritten. - A non-text key, an alias that fans out past 2,000 values, or a change that only an alias holds is refused with the reason.

  • TOML.

- It is patched through @decimalturn/toml-patch, which keeps comments and layout. - Tables become fieldsets and arrays of tables become cards. - Dates show as the text they were written as. - TOML has no null, so a form that would write one is refused.

  • Safety check on save. After a YAML or TOML write, the result is read back and must equal what the form held. If it doesn't, nothing is committed.
  • Schema lookup. app.yml and app.toml find a sibling app.schema.json.
  • Vendored libraries. Both are bundled under render/vendor/ with their licences (ISC and MIT), the same way og/vendor holds resvg. They run on the server only, so no script reaches the page.

Tests: test/config-yaml-toml.test.ts covers the round trip, the single-line change, multi-document YAML, an alias bomb, TOML tables and cards, dates, and the null refusal. test/config-form.test.ts now also commits a YAML edit over HTTP.

What I chose not to do

  • YAML sequence indent is one setting per document, so a file that mixes flush and indented sequences has its changed document re-indented. This is documented in docs/features/config-form.md.
  • The terminal's schemaRefusal check still covers JSON only.
  • Reordering keys in an object is not an edit.
Changes requested — before the latest push

Changes requested. Fix in one round:

  1. Direction: the owner's order is PIPELINE FIRST (#6534, #6611, #5276, #3316, #6217, #6428, #1242, #1073, then impact gating). A new config-form feature (#7337) is not on that list, and it adds 13,000+ lines of vendored code that makes fast.ts name 1,129 files, so every later pull's fast path is slower. The builder says it skipped the full suite for this reason. Rebase and hold the pull until the pipeline items land, or at least mark it clearly as parked.
  1. Both doors: the terminal (src/tui/screens/workspace.ts schemaCheck -> schemaRefusal in json-schema.ts) still returns early for non-.json files. A YAML or TOML file committed from the terminal skips the schema check the page now applies. The builder named this as left undone. Make schemaRefusal format-aware via parseConfig, with a test, or the page and terminal disagree.
  1. Dependencies: two large bundles (yaml, toml-patch) are vendored. The pull must justify each in the pull description and docs, and record how to regenerate them (pinned version, source and command). The header comment only names a version. Also add a test that checks the bundled license files match what is vendored, or at least a note in the README/docs on updating them.
  1. Clean: toml-config.ts has an empty catch {} and reduce<any>. Replace them with a typed walk and a handled path. config-format.ts still exports LANGUAGE with a ?? "json" fallback repeated at three call sites: put it in one languageOf(path) function.
  1. Security: the YAML alias fan-out cap (2,000 values) is good, but add tests showing the cap also holds for deeply nested flow collections ([[[[...]]]] at depth 10,000) and for YAML merge keys << and for TOML at depth 64+, in both the page and the submit path. State in the tests that parsing is bounded by MAX_FORM_BYTES before parsing, and verify that parseConfig runs only after the size check in configSubmit (current: readText(..., MAX_FORM_BYTES), OK, but pin it with a test).
  1. Preview: / answers 200, but I could not see a YAML/TOML repository file on this preview to exercise the form with. Add a fixture .yml and .toml to the repo's docs or examples so the pull can be tried as described.

The design touch is small and reuses the existing form, so no design-system change is requested.

commented

Config files (YAML and TOML) open as a form with their comments kept, and the review items 2–6 are fixed. Item 1, holding this pull until the pipeline items land, is not something I can do from here, so it needs a person's call. bun scripts/gate.ts, tsc --noEmit, design-lint, test/config-form.test.ts, test/config-yaml-toml.test.ts and test/tui-propose-edit.test.ts pass. I did not run the full suite; the forge's check does that on push.

Fixed (items 2–6)

  • Terminal and page agree (2). schemaRefusal now goes through isFormConfig and parseConfig, so the terminal's commit check covers .yml, .yaml and .toml as the page does. A new test shows a YAML and a TOML file that break a sibling schema are refused and ones that keep it are not.
  • Clean-up (4). LANGUAGE is no longer exported. languageOf(path) in config-format.ts is the one place that applies the JSON fallback, and the three call sites use it. toml-config.ts now walks the tree with a typed parentOf. A path the tree doesn't have returns the "cannot be written" message. The catch in the date check returns undefined with a comment. The reduce<any> is gone.
  • Hostile input (5). Both the page and the submit path now have tests for these cases:

- YAML flow collections nested 10,000 deep. - A YAML merge-key (<<) fan-out. - TOML nested 70 deep. - A file over MAX_FORM_BYTES, refused before it is parsed.

I also added unit tests for the same cases, including the TOML boundary (20 deep parses, 65 deep is refused).

  • Dependencies (3). docs/features/config-form.md now has a table giving, for each bundle, the pinned version, licence, and why it is needed. It also gives the bun add and bun build commands to regenerate them.
  • Version check (3). A test checks that each bundle's header version matches that page, and that the two licence files are present.
  • Try it (6). docs/examples/config/service.yml and service.toml are fixtures with comments, an anchor and merge key, a date, and an array of tables. The docs have a "Try it" section. Once the preview is up, open either file on its blob page, press Edit as form, change one value and Review. The diff should touch only that line.

Not done

  • Direction (1). I can't land or park the pull myself. The vendored bundles are what make fast.ts name 1,129 files (the output says "expected 97m49s"). Please decide whether to hold the pull until the pipeline items land or merge it as it is. I haven't changed any labels.
  • Regenerate commands (3). I wrote them from the bundle headers and the bun build output style, and did not re-run them here. The sandbox can't confirm a byte-identical rebuild.

🤖 Generated with Claude Code

Changes requested — before the latest push

Changes requested. Fix in one round:

  1. Direction (first): the owner order is PIPELINE FIRST (#6534, #6611, #5276, #3316, #6217, #6428, #1242, #1073, then impact gating). A new config-form feature (#7337) is not on that list, and it adds ~13,000 lines of vendored bundles that make fast.ts name 1,129 files (the builder skipped the full suite because of it), slowing every later pull. Park it until the pipeline items land, or shrink it: do not vendor 13k lines to land a feature ahead of the pipeline.
  1. Both doors: the form editor has no screen in src/tui/ (nothing under src/tui references config-form or the YAML/TOML writers). The terminal only gets the schemaRefusal check at commit. A person who can edit a YAML/TOML value as a form on the page must be able to do it in the terminal, with an entry where AGENTS.md says, live to web viewers. Add the screen (or state the issue scopes it out and link the follow-up issue).
  1. Dependencies: ~13k lines of third-party code is committed as a bundle (yaml.mjs, toml-patch.mjs). The justification is in the docs, but the regenerate commands were not re-run by the builder, so reproducibility is unproven. Verify a byte-identical rebuild, or add a check that pins the bundle hash.
  1. Preview: / answers 200, but no .yml/.toml file is reachable on the preview repository to exercise the form; docs/examples files exist only in the repo. Show the form working there, and confirm the form pages keep the design-system tokens and work with JavaScript off (plain form POST).

Positives: parse bounded by size/depth/alias budget, read-back check before commit, one language switch in config-format.ts, tests added, README untouched (how-to is in docs/features/config-form.md).

commented

This round adds a pinned hash check for the two vendored bundles and puts a YAML and a TOML file on the preview. A terminal form screen is split off to #8934 because JSON doesn't have one either. I disagree with parking or shrinking the pull, for the reasons below. A person needs to make that call.

Direction (1).

  • The bundles don't slow fast.ts. I measured both halves of the diff. With only yaml.mjs and toml-patch.mjs changed, fast.ts names 7 test files. With everything except them changed, it names 1,129. The wide reach comes from touching the blob page's renderers, json-schema.ts and the terminal workspace, which the server imports. Any change to the blob page reaches that far.
  • Vendoring is what the issue asks for. #7337 and its umbrella #7329 call for a vendored TOML parser and the yaml package's Document API, copied in the way src/web/og/vendor is. #7329 quotes the owner wanting this "this week".
  • Whether to hold it behind the pipeline items is the owner's call. I haven't parked it myself.

Terminal (2).

  • Nothing in src/tui/ has a form screen for config files, JSON included. JSON's form (#7336) is already on main without one.
  • I opened #8934 for a terminal form for all three languages, using the same fields, writers, read-back and proposal path as the web, and live to web viewers.
  • #8934 waits for #7337, and the umbrella #7329 now waits for #8934. docs/features/config-form.md says the terminal has no form yet and points to #8934.
  • What the terminal does have in this pull is the schema check at commit for YAML and TOML.

Reproducible bundles (3).

  • I ran the documented commands in a scratch directory, with the pinned versions from the registry. Both bundles come out byte-identical to what is committed, once --target=bun is used and bun's // @bun first line is swapped for our header. The upstream licence files match the vendored ones exactly.
  • The doc now has the exact commands and each bundle's SHA-256. test/config-yaml-toml.test.ts fails if a bundle's hash, its header version or the doc disagree.

Preview (4).

  • src/spawn/preview-config.ts holds service.yml and service.toml. I moved them there from docs/examples/ because docs/ isn't in the image.
  • spawn/preview-seed.ts commits them into the preview's sample repository once, including a sample seeded before they existed, since preview data survives a redeploy. test/looks-owner.test.ts covers this.
  • On this preview, /preview-admin/sample/blob/main/service.yml and service.toml both draw class="card config-form". The YAML form has fields like v/web/port and v/tags/1; the TOML form has v/web/started and the v/servers/N cards.
  • The form is a plain <form method="post">, now asserted in a test, and the existing save tests post it without JavaScript. Design-lint passes.

gate.ts and tsc --noEmit pass, and so do test/config-form.test.ts, test/config-yaml-toml.test.ts, test/looks-owner.test.ts and test/signup-gate-parent.test.ts. I didn't run the full suite here because fast.ts names 1,129 files; the forge's check runs it.

To try it, open /preview-admin/sample/blob/main/service.yml on the preview, press Edit as form, change port, and press Review. The diff should touch one line and keep the comments and the &defaults anchor.

🤖 Generated with Claude Code

Approved

Earlier asks: (2) terminal schema check now covers YAML/TOML via isFormConfig/parseConfig, with a test: met. (3) vendored bundles have pinned version, licence, regenerate commands and a SHA-256 test: met. (4) clean-up of LANGUAGE/languageOf, typed parentOf, handled catch: met. (5) hostile-input bounds and size-before-parse: met. (6) service.yml and service.toml are seeded on the preview; both blob pages return 200 and draw the config form: met. (1) direction: the owner-order question was put to a person, not something the builder can settle; the vendored bundles are not what widens fast.ts, per the builder measurement. I did not re-verify that number. Non-blocking: (a) the terminal still has no form screen; #8934 tracks it. (b) Pin the vendored bundles with the hash test as written and keep the next bump to its own pull. The commit was not in my clone so I read the current diff, not the delta since 452bd7105b6a.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs