Skip to content

build(scripts): guard the generated markdown tables with a round trip instead of a regex - #457

Merged
Vivswan merged 1 commit into
mainfrom
build/markdown-region-guards
Oct 5, 2026
Merged

Vivswan merged 1 commit into
mainfrom
build/markdown-region-guards

Conversation

@Vivswan

@Vivswan Vivswan commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Before

A pipe typed into the Sections table's labels Notes cell, then bun run build:docs:

main's regex admits the edited body: true
regenerated page drops the edit:     true

After

the sections-table region in docs/reference/sections.md encloses content the generator would not write;
move its marker back (line 4 has 6 cells where the table has 5)

How

  • One round trip for both tables. tableRoundTrip splits by the inverse of tableRow(), applies the table rule, re-renders, byte-compares.
  • One statement per table, both sides consult it. tableFault is thrown by renderTable and refused by the guard; policyTableFault adds the Defaults prose rule.
  • Property tests hold both sides to it: the renderer throws exactly the statement's fault, the guard admits what was written and refuses faulted cells joined without the renderer.
  • A rule test per registry refuses a RegExp body on a table region.

Proof

  • Refusal tables: 10 Sections and 14 Defaults rows, red against main first: 9 admitted on main, 15 refused generically.
  • Property inputs: 339 Defaults, 576 Sections.
  • Gates: tsc -p ., biome, knip, bun test test/scripts (415) green; both generators regenerate unchanged.
Technical details

Line accounting (git diff --numstat against the merge base):

Kind Added Removed
Source (lib/markdown-table.ts, lib/generated-regions.ts, gen-docs.ts, gen-action-docs.ts) 271 55
Tests (gen-docs.test.ts, gen-action-docs.test.ts) 348 14
  • The Defaults table re-renders through its real renderer, its parser sharing proseFault (trimmed-blank) and OPPOSITE with it.
  • The Sections table re-renders through the table helper with prose cells opaque, since its PAT cell paraphrases prose no parser inverts.
  • Negative controls: a renderer-only rejection of a padded Notes cell, a guard-only rejection of the same, a renderer-only rejection of a padded Defaults caveat, and a guard-only rejection of the untouched display each turn a property test red.
  • Dropped: a compile-time proof of non-blank prose. A structural type cannot keep a hidden property out of a parameter, so the run-time statement is the boundary, and the renderer reads each row once so the check and the cells see the same values.
  • Recorded, not built: the rule test selects table regions by a rendering that starts with \n|, and refuses a RegExp body without proving a function body is tableRoundTrip.
  • Recorded, not built: accessor probes (a cell getter or an undeclaredDefault getter changing between reads) need a hostile accessor on data this repository authors itself, which no outside input can supply.
  • Recorded, not built: matrix extensions beyond the grammar's dimensions (PAT grant and caveat variants in the Sections matrix, same-policy rows in reverse key order in the Defaults matrix).
  • Reviewer note (Copilot): renderedMismatch is the former inline tail of the action.yml roundTrip, moved without a behavior change.
  • Files: .github/scripts/lib/{markdown-table.ts,generated-regions.ts}, .github/scripts/{gen-docs.ts,gen-action-docs.ts}, test/scripts/{gen-docs.test.ts,gen-action-docs.test.ts}

Copilot AI balanced review requested due to automatic review settings October 5, 2026 05:07
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

File size check

0 over a hard cap (fails), 10 warning(s).

File Size Tier Cap
.github/scripts/gen-inputs-table.ts:54 11 comment lines warn 10
.github/scripts/release-pipeline.ts:1 34 comment lines (header) warn 25
.github/scripts/release-pipeline.ts:453 14 comment lines warn 10
docs/upgrading/v2-to-v3.md 1292 lines warn 1040
src/github/secret-scan.ts:45 11 comment lines warn 10
src/sections/contract/errors.ts:13 14 comment lines warn 10
src/sections/shared/roles.ts:43 13 comment lines warn 10
test/e2e/generators.ts 2634 lines warn 2560
test/e2e/generators.ts:1667 12 comment lines warn 10
test/sections/refusal-messages.test.ts:61 19 comment lines warn 10

Split the file, wrap the line, shorten or exempt the comment, or list the path in .file-size-allow.local with a # reason.

5 managed file(s) skipped; repo-platform owns them.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The guards consistently mirror their renderers and the tests cover the relevant malformed-table cases.

Review effort: Balanced
Findings: None

What changed in this PR

Replaces regex-based generated-table guards with round-trip validation to prevent malformed Markdown from being silently overwritten.

Changes:

  • Adds reusable Markdown table parsing, rendering, and round-trip validation.
  • Applies round-trip guards to the Sections and Defaults tables.
  • Expands tests for malformed rows, duplicate keys, ordering, and renderer compatibility.
File Description
.github/​scripts/​lib/​markdown-table.ts Adds shared table rendering and round-trip guards.
.github/​scripts/​lib/​generated-regions.ts Extracts reusable rendered-body comparison.
.github/​scripts/​gen-docs.ts Guards the Sections table through round-trip validation.
.github/​scripts/​gen-action-docs.ts Guards Defaults and enforces nonempty policy prose literals.
test/​scripts/​gen-docs.test.ts Tests Sections table acceptance and refusal cases.
test/​scripts/​gen-action-docs.test.ts Tests Defaults table round trips and malformed bodies.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI balanced review requested due to automatic review settings October 5, 2026 06:16
@Vivswan
Vivswan force-pushed the build/markdown-region-guards branch from 4d9cfbd to 5a71dfe Compare October 5, 2026 06:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Whitespace-only Defaults prose still bypasses the new empty-prose validation.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)

Comment thread .github/scripts/gen-action-docs.ts Resolved
Copilot AI balanced review requested due to automatic review settings October 5, 2026 07:07
@Vivswan
Vivswan force-pushed the build/markdown-region-guards branch from 5a71dfe to f950507 Compare October 5, 2026 07:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The shared grammar consistently rejects non-round-trippable table content and is covered by focused tests.

Review effort: Balanced
Findings: None

Resolved since last review (1)

Copilot AI balanced review requested due to automatic review settings October 5, 2026 07:46
@Vivswan
Vivswan force-pushed the build/markdown-region-guards branch from f950507 to 006cc61 Compare October 5, 2026 07:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The shared validation logic is consistently applied and comprehensively tested against malformed and valid table content.

Review effort: Balanced
Findings: None

@Vivswan
Vivswan marked this pull request as ready for review October 5, 2026 09:26
@Vivswan Vivswan added the merge-when-green Owner approved: merge once every gate is green label Oct 5, 2026
@Vivswan Vivswan closed this Oct 5, 2026
@Vivswan Vivswan reopened this Oct 5, 2026
… instead of a regex

The sections-table and policy-defaults-table regions admitted authored text their renderers never write, so a hand edit inside the markers was erased on the next regeneration instead of refused. The sections-table shape used `[^\n]*` cells and admitted a pipe, a carriage return, a sixth cell, a blank cell, a repeated section with other prose, and an Undeclared default outside the three displays. The policy-defaults-table shape admitted an override naming the same policy as the default, a keep row above a delete row, and a repeated section.

Both shapes are now one shared round trip, `tableRoundTrip` in lib/markdown-table.ts: the body splits into rows and cells by the inverse of `tableRow()`, the rows pass the table rule (the header's column count, the cell rule `tableCell()` writes by, a key span opening every row, no key repeated), a per-table parser turns the cells into the record the table renders from, and the records re-rendered must reproduce the body byte for byte. The refusal names the first differing line through `renderedMismatch`, now shared with the action.yml round trip. The Defaults table re-renders through its real renderer, so the delete-first order comes from the renderer, and its parser checks the opposite-policy binding through the same `OPPOSITE` table the renderer writes from. The Sections table re-renders through the table helper with the prose cells opaque, since its PAT cell paraphrases prose no parser inverts.

The helper is the grammar on both sides. `tableFault()` is the one statement of the table rule: `renderTable()` throws its text and `tableRoundTrip()` refuses by the same rule, so a renderer cannot write a body its own guard would refuse, which would otherwise turn the next regeneration of the committed page into a throw. The Defaults statement `policyTableFault()` adds each row's prose by `proseFault`, the trimmed-blank rule `tableCell()` uses, which the parser consults too, since blank prose renders `delete ()` or a bare policy span that the cell rule admits. A compile-time proof of non-blank prose was tried and dropped: a structural type cannot keep a hidden property out of a parameter, so only the run-time check closes the boundary.

Two property-style tests hold both sides to the statement over generated inputs (keys inside and outside the key span, both policies, prose with blanks, padding, pipes, carriage returns, and a Unicode separator, duplicate and reordered sections): the renderer throws exactly the statement's fault, the guard admits what the renderer wrote, and the guard refuses the same cells joined without the renderer whenever the statement faults, so a check added to one side only turns a generated input red. A rule test in each registry's suite refuses a RegExp body on any region whose rendering is a table. Refusal tables for both regions pin each input above: nine were admitted by the regexes on main, the rest were refused with the generic message and now name the cell or line.
@Vivswan
Vivswan force-pushed the build/markdown-region-guards branch from 006cc61 to 0ca98e2 Compare October 5, 2026 10:58
@Vivswan Vivswan closed this Oct 5, 2026
@Vivswan Vivswan reopened this Oct 5, 2026
@Vivswan
Vivswan merged commit 3a3198b into main Oct 5, 2026
51 of 53 checks passed
@Vivswan
Vivswan deleted the build/markdown-region-guards branch October 5, 2026 12:39
@Vivswan
Vivswan restored the build/markdown-region-guards branch October 5, 2026 12:43
@Vivswan
Vivswan deleted the build/markdown-region-guards branch October 5, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-when-green Owner approved: merge once every gate is green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants