You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
build(scripts): seal generated regions behind a parse-render round trip - #483
GeneratedRegion.of() is the one way to build a region, so its body check is always its own round trip.
How
Sealed constructor: private, so a body: regex no longer typechecks.
Round trip as the check: a body must parse and re-render byte for byte.
Seven regex bodies became parsers, and action.yml reads back through yamlDeclarations().
Composer tables render through renderTable(), bytes unchanged.
Line numbers count down from the BEGIN marker.
A count past the word table is refused at the parser naming line 1, never thrown by the renderer.
Proof
Gates: typecheck, biome, lint:arch, knip clean, both generators idempotent.
Tests: 190 pass in the four touched files. A planted hand edit in ten generated pages and a 21-key count sentence are refused naming the line, and dropping the composer's escaping fails 3.
Guarantee's scope: a region declared through this module cannot carry a body check other than its own parse-render round trip, and nothing stops a script from bypassing the module entirely, which is review's job.
Why the scripts grew: eight read-back parsers stand where seven regexes and the local YAML round trip stood, one per renderer.
Censuses, where:grep -c '^\s*body: ' over the two generator scripts, grep -c 'function roundTrip' over markdown.ts and the scripts, grep -c '\| '` over composer.ts.
Deleted tests: "a region that renders a table carries a round trip, never a hand-written grammar" in both generator test files, since the type makes a RegExp body unrepresentable and the body: census stands in.
New test, numbering: the refusal's line number equals the edited line's distance from the BEGIN marker, pinning the message and each parse to one convention.
New tests, architecture map: an authored line inside the mermaid fence and one after it are refused, and a dotted layer id reads back as the renderer writes it.
New tests, prose lists: a malformed list is refused naming its line instead of throwing from the renderer, and the renderer refuses an item holding the word "and".
New test, count sentence: a page listing 21 keys under "Twenty-one sections ..." is refused with line 1 lists 21 section keys, past the 20 the count words name; on the prior code the test failed with countWord()'s throw.
Count bound:COUNT_WORD_MAX is the one exported bound beside the COUNT_WORDS table (.github/scripts/lib/count-word.ts), derived from its length, and the parser reads it. The registry path keeps its throw: a derived list outgrowing the table is a programming error, not a page a user edited.
Kept behavior: the Defaults table's delete-first order is still checked, by renderPolicyRows() sorting the parsed rows before the byte compare.
Kept behavior:policyRows() walks sections delete-first so a missing-prose error names the same section as before.
Coverage page: its prose slots stay opaque to the parse (earlier design), so a hand edit inside a paragraph regenerates rather than refuses, and a structural edit (a fourth cell in a Supported row) is refused naming its line.
YAML lines:yamlDeclarations() parses with prettyErrors: false and counts the error offset in the body, so the named line is the region's rather than the library's, which counts the prepended key line as 1.
Prose lists:proseItemFault() is the one rule (no comma, no standalone "and") that both the renderer and the read-back refuse by, for labels and route spans alike, so every ", " and " and " in output is a joiner and the split is exact.
Refused: a duplicate-tolerant node list for the architecture map. Two layer names differing only in "-" and "_" collide in renderArchitectureMermaid()'s id mapping (arch-lint.ts, outside this change), mermaid would merge them into a wrong diagram, and the region now refuses that page instead. An injective id belongs to arch-lint.ts.
Recorded, not built: the YAML-conversion and declaration-validation refusals in gen-action-docs.ts carry no numeric line (inherited behavior); architecture source-equality, where a swapped edge round-trips and regenerates rather than refusing; runtime sealing against Object.create or method replacement; empty registries the data thunks never supply; class-wide coverage that would catch a future parser admitting data its renderer throws on, beyond the prose-list and count guards built here.
A generated region is built from a parse and a render through GeneratedRegion.of(); the body check is that round trip, so no region can carry a grammar written beside its renderer. The action.yml regions parse through yamlDeclarations() instead of a local roundTrip(), the composer's two tables render through renderTable(), and a refusal says its line counts down from the BEGIN marker.
…ndering it
parseCountSentence() admitted any number of section keys, so a hand-edited count sentence listing 21 reached the renderer, whose countWord(21) threw with no region or line named. The parser now refuses a count past COUNT_WORD_MAX, the one exported bound beside the COUNT_WORDS table, naming line 1 like its other refusals. The registry path keeps its throw: a derived list outgrowing the table is a programming error.
Also wraps the one test line over 256 characters and re-cuts the comment over 120 columns the PR added.
Whitespace layer names produce invalid Mermaid output
.github/scripts/gen-docs.ts:767
A valid layer name containing whitespace makes the generator reject its own freshly rendered architecture map.
For example, renaming main to main layer (and updating its edges) is accepted by Architecture and renders main layer["src/main.ts"], but NODE_LINE rejects that line because the ID is limited to \S+. renderArchitectureMermaid() only rewrites hyphens, so either parse every ID it can emit or validate/encode layer names before rendering; add a whitespace-ID round-trip case alongside the dot/digit case.
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
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.
Before
After
GeneratedRegion.of()is the one way to build a region, so its body check is always its own round trip.How
body:regex no longer typechecks.yamlDeclarations().renderTable(), bytes unchanged.Proof
lint:arch, knip clean, both generators idempotent.body:literals 12 -> 0,roundTripdefinitions 2 -> 1, hand-built|rows in composer.ts 7 -> 0.Technical details
grep -c '^\s*body: 'over the two generator scripts,grep -c 'function roundTrip'over markdown.ts and the scripts,grep -c '\| '` over composer.ts.body:census stands in.line 1 lists 21 section keys, past the 20 the count words name; on the prior code the test failed withcountWord()'s throw.COUNT_WORD_MAXis the one exported bound beside theCOUNT_WORDStable (.github/scripts/lib/count-word.ts), derived from its length, and the parser reads it. The registry path keeps its throw: a derived list outgrowing the table is a programming error, not a page a user edited.renderPolicyRows()sorting the parsed rows before the byte compare.policyRows()walks sections delete-first so a missing-prose error names the same section as before.yamlDeclarations()parses withprettyErrors: falseand counts the error offset in the body, so the named line is the region's rather than the library's, which counts the prepended key line as 1.proseItemFault()is the one rule (no comma, no standalone "and") that both the renderer and the read-back refuse by, for labels and route spans alike, so every ", " and " and " in output is a joiner and the split is exact.renderArchitectureMermaid()'s id mapping (arch-lint.ts, outside this change), mermaid would merge them into a wrong diagram, and the region now refuses that page instead. An injective id belongs to arch-lint.ts.Object.createor method replacement; empty registries the data thunks never supply; class-wide coverage that would catch a future parser admitting data its renderer throws on, beyond the prose-list and count guards built here..github/scripts/{gen-docs.ts,gen-action-docs.ts,lib/generated-regions.ts,lib/count-word.ts},src/report/{markdown.ts,composer.ts},test/scripts/{generated-regions,gen-docs,gen-action-docs}.test.ts.