Repository navigation
feat: forge normalize, a command that rewrites other tools' extensions (FORGE-1) - #72
Draft
Swimburger wants to merge 25 commits into
Draft
Swimburger wants to merge 25 commits into
Swimburger wants to merge 25 commits into
Conversation
…k (FORGE-1) Add the metadata resolution layer of RFC 0001. - src/metadata: the native x-forge-* vocabulary, per kind of object, as pure key tables, primitives and value-format descriptors. - src/compat: the compatibility layer, behind the default-on `compat` feature. The Fern and Stainless adapters map their keys onto the same primitives; every vendor name lives here. - src/link/resolve.rs: native first, compat second. A native key's value is kept over a compat one's, whatever order they are written in. - Errors name the key the document wrote. - Docs and the WIT speak the native keys; the WIT copies are regenerated (doc comments only, so forge:plugin stays at 0.3.0). Output is unchanged: the unmodified Fern-keyed suite passes against it, and the real aggregate builds the same files and the same model dump.
x-fern-* and x-stainless-skip become their x-forge-* equivalents; a dotted group string becomes an array. What only the compat layer does is covered by tests/compat.
tests/boundary.rs fails, naming the file and line, when an x-fern or x-stainless name appears in src/, tests/ or packages/forge/ outside src/compat/ and tests/compat/. `just boundary` runs it.
- Each table checks its prefix (x-forge-, x-fern-, x-stainless-) before matching, and only x- keys are resolved, so a skipped key costs one compare instead of a match per table. - A Hit holds only its field and slot; a key's name comes from the slot, in the per-kind name tables, when an error needs it. The reader's per-key result is back to 16 bytes, which fits in registers. Output and the model dump of the aggregate are unchanged.
Builds REV in a throwaway worktree and runs the parse bench of both in ABBA order, so drift in the machine's speed cancels, leaving out pairs in which it changed speed. `just bench-compare [rev] [pairs]` runs it.
…tive - x-forge-method-name, in line with the other -name keys. x-forge-method never shipped, so it gets no alias. - x-forge-sensitive replaces x-sensitive, so every native key begins with x-forge-. x-sensitive stays readable as a deprecated native alias: a new Tier (native, deprecated, compat) decides which key's value is kept, and the alias works with compat off. - tests/boundary.rs also catches vendor names spelled as identifiers (x_fern_...), and the five test names that still had them are renamed. Output and the model dump of the aggregate are unchanged.
…ecision number - Rule 4 now says per key what an empty or punctuation-only value does: names fall back to the wire name; an empty method name or group path is no method, and a punctuation-only one is an error. - The npm release adds no compat module: the engine binary it ships is built with the default compat feature. - "the D4 rule" is resolution rule 2; the D-numbers were never defined.
Today x-forge-group: [] makes no method. The intended meaning is a method at the root, which needs root methods in the model, the WIT and the generators, so it is documented as a known gap rather than fixed here.
… checked - In an alias, the native method name, group, availability and ignore win over Fern's, in either order. - A Fern group path or method name that is an error on its own is not checked when a native key wins. Breaking the precedence rule fails all three.
- The boundary goal covers the engine; the docs packages read vendor keys themselves and are a non-goal. - resolve.rs is the only place link calls compat, not the only cfg point. - Deleting the layer lists every step, and what it leaves behind. - With compat off, vendor keys become ordinary extensions for plugins. - The name lookups are a second way vendor key names leave compat, as data, for errors. - Test counts, the mutation result, and performance and memory are re-measured on the final code.
- Rename x-forge-ignore to x-forge-skip, internal names included. - Rename x-forge-group to x-forge-group-name, to match the other -name keys and stay apart from the legacy x-forge-group-info. - Remove benches/compare.sh and its just recipe; the RFC describes the method inline. - RFC: record why the key is model-name, and that CI compiles but does not test the build without compat, on purpose.
AGENTS.md gets a short section: only src/compat/ names vendor extensions, it sits behind the compat feature, native keys win, and tests/boundary.rs enforces it. src/compat/README.md holds the longer how-to, next to the code it describes and deleted along with it.
Each level (operation or alias, parameter, schema, document) now has its own module holding its key constants, its table of names and its reader, in the native vocabulary and in each adapter. A key constant is no longer in scope at a level it does not belong to. Tests check that every key is read at exactly the levels that list it. Also drop the key tables from the adapters' module docs: the code is the source of truth for the keys.
Drop the status header, the known gaps and follow-ups sections, the test plan and the release shape, and shorten the rest. The vocabulary, the resolution rules, the architecture and the alternatives considered stay.
A group path written as a string is names separated by dots, and one written as an array is the names themselves, which are not split, so ["a.b", "c"] is two names and "a.b.c" is three. `forge normalize` writes an array as a dotted string only when it says the same thing, so it depends on the two readings staying as they are.
… ones Rewrites the extensions other tools' metadata is written with into the native x-forge-* ones, in a JSON document, using the keys the compatibility layer reads: a key is only rewritten where `forge` would have read it, so it never gets a meaning it did not have. FORGE-1. The document is edited as bytes and not written again, so everything but the keys that change, such as the text of every number, the whitespace and the order of the keys, stays as it was. It is walked once, without a tree: 28 MB in about 20 ms. - Native keys are never replaced. When a document says the same thing twice, the native key wins, and the other is reported if it says something else. - A group path that is an array becomes the names joined by dots, unless a name has a dot in it, which a dotted string cannot say. - What no native key says, such as a skip that is a list of targets, is left as it is written. - A key written twice next to a key that is rewritten is an error, as `forge` reads no key that is written twice. - The document is checked to be JSON beside the walk, on another thread when it is long, so that what is written does not have to be. - `leave_old_extensions` keeps each key that is rewritten, beside the native key that replaces it. The native key of each field is named by the per-level modules of `metadata`. The feature is `normalize`, on by default, and needs `compat`.
`forge normalize [--leave-old-extensions] [--check] [-v] <INPUT>...` rewrites other tools' extensions into the native ones, in place. FORGE-1. An input is a file, a folder (every .json file below it, except hidden ones and those in a node_modules), a glob pattern, which is taken as it is written, or - for standard input to standard output. - One line for each file that is rewritten, and a summary. `-v` adds how many times each key was rewritten. Conflicts are warnings. - `--check` writes nothing and fails if anything would change, to check that documents are normalized. - Files are rewritten by writing a new file beside them and renaming it, so that none is left half written, and in place where that is not possible, as for a hard link. A file with more than one name is rewritten once, and a link stays a link. - A file that is not valid JSON, is read-only, or is YAML is an error and is left as it is. `normalize` is a subcommand of clap's: `args_conflicts_with_subcommands` lets `forge <SPEC> --target ...` and `forge normalize ...` be told apart. A document that is itself called `normalize` is given as `./normalize`.
The package README says how to use it and what it leaves alone, and the compatibility layer's README says how it is built on the adapter tables, what it does that is not a key becoming its native key, and what to delete to remove it with the layer.
This was referenced Oct 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the review comment on #69 that vendor compatibility should be a separate command and not something the generator does silently. FORGE-1.
What
forge normalizerewrites the extensions other tools' metadata is written with into the nativex-forge-*ones, in place:It is meant as a step before
generatein CI, and for teams migrating their specs. It is also a library:forge::normalize::normalize.How it behaves
forgereads, so there is no second list of keys, and a key is only rewritten whereforgewould have read it.forgereads no key that is written twice.node_modules.normalizeis a clap subcommand.forge <SPEC> --target ...still works. A document that is itself callednormalizeis given as./normalize.Performance
On the 28 MB released aggregate (release build): about 28 ms for a first run, about 21 ms when it is already normalized, and 24 ms for
--check. The document is walked once with no tree, and checked to be JSON on a second thread while it is walked.Testing
--check, links, permissions).Forgemodel before and after, in both modes, with no conflicts. That test is#[ignore]and needsFORGE_SPEC:FORGE_SPEC=openapi.forge.json cargo test --release --test compat aggregate -- --ignored-D warnings) with all features and with none.Not in this PR
--leave-old-extensions.