Repository navigation
fix(engine): warn of a _layering or _remove that nothing reads - #605
Conversation
Two directive spellings ran with nothing said. A single document whose top-level _layering is outside "replace", "shallow", "deep" completed silently, because nothing folds a single document and so nothing read the value, while mode: render refused the same file as a layer. A _remove on an entry of a list the registry does not key (a ruleset's bypass_actors, a rule's parameter lists, check_suite_preferences.auto_trigger_checks, any list under a passthrough key) removed nothing and passed through to GitHub as a field named _remove, from a single document and from a layer alike. Both runs complete today, so neither is refused before the next major: each now draws a warning naming the source and the path, and the two accepting paths in validateSettingsDoc carry COMPAT(v4) markers naming the refusal that replaces the warning. separateRemovals, the one walk over a document's removals, now also gathers every _remove on an entry no list keys: below the mapping sections, and below each kept keyed entry outside its declared nested lists. Each path is spelled from its steps as validation spells an issue's, so a key that only looks like a list step is quoted and the way back to the document as written never renumbers through it. A layer is judged as its standalone view, so validateLayer hands validateSettingsDoc the view's way back and a layer's warning names the entry by its index in the file. The wording lives in engine/directives.ts beside the directive keys, and admit.ts now reads the one isLayering predicate from there.
The two warnings printed before the document's issues were known, so a refused document drew them too. For a marker in a closed entry shape (a branch protection's required_status_checks.checks item) the run printed the shape's refusal and, beside it, a warning that the key "passes through as a field" and that "the next major version refuses it": the first untrue of a key just refused, the second already done. validateSettingsDoc now prints both warnings once the document has no issue. A document that validates has no removal site, so its paths are the document's own and separateRemovals no longer renumbers them itself; a layer's standalone view still hands over its way back. The case that expected a warning beside a refused keyed removal is replaced by two cases pinning that a refused document draws none, and the quoted-key case now runs through a layer's view, the one place a removal may stand beside the marker in a document that validates. Two lines over the 256-character cap are brought under it: a doc comment, and the pinned warning text in the test.
…nt's The page said of a _remove on an entry of a list that is not keyed that "the run warns". A document the shapes refuse draws no warning, since a closed entry shape names the marker in its own refusal, so the sentence now says a document that validates draws it.
File size check0 over a hard cap (fails), 6 warning(s).
Split the file, wrap the line, shorten or exempt the comment, or list the path in 5 managed file(s) skipped; repo-platform owns them. |
Member shape check16 finding(s).
Add the blank lines, make the comment one line directly above the item, comment every field or none, or list the path in 4 generated file(s) skipped. |
There was a problem hiding this comment.
🟡 Changes recommended
Shared passthrough objects cause exponential traversal, and rejected layers incorrectly emit _remove warnings before validation.
2 open issues
What changed in this PR
This PR adds warnings for ignored _layering values and ineffective _remove markers in GitHub Settings as Code, while preserving current acceptance behavior until the next major version.
Changes:
- Detects
_removemarkers in unkeyed lists and emits warnings after document validation. - Maps layer warning paths back to the original file's entry indices.
- Adds regression tests and documents the new warning behavior.
| File | Description |
|---|---|
test/flows/layers.test.ts |
Tests original-file indices and rendered-document warnings. |
test/engine/orchestrate.test.ts |
Tests warnings, refusal controls, quoted paths, and cycles. |
src/flows/layers.ts |
Passes original-file path mapping into validation. |
src/engine/separate-removals.ts |
Collects markers in unkeyed lists during traversal. |
src/engine/orchestrate.ts |
Emits warnings after document checks succeed. |
src/engine/directives.ts |
Defines warning messages and shared layering validation. |
src/engine/admit.ts |
Reuses the shared layering predicate. |
docs/operate/layering.md |
Documents warnings for ineffective removal markers. |
Review effort: Balanced
A layer is validated as its standalone view, which sets aside what the fold alone judges, so its warning for a _remove no list keys went out before the fold refused the layer: a top-level _layering outside its values, a keyed marker that is not true, a wrapper's bad _layering, or another layer the shapes refuse. The line said the key "passes through as a field" of a render that then wrote nothing. foldLayers now routes every validation of the fold, each layer's and the merged document's, through capturingIo and replays the held lines in their recorded order once the whole fold stands; a refused fold drops them. A fold that stands prints what it printed before, in the same order: the layers' warnings in layer order, then the merged document's. Three scenarios pin the two warnings in the shipped bundle: a single document whose top-level _layering is outside its values, a single document with a _remove on an entry no list keys, and a layered render where a keyed removal precedes such an entry, so its path counts the removal. The first is raw text: the published schema's enum refuses the value the run still accepts until the next major, and the corpus's schema check holds every settings mapping to that schema.
…keyed _remove The walk for a _remove on an entry no list keys visited a shared object once per path. A mapping aliased under two keys, 24 levels deep, is 25 mappings and 33,554,431 paths, so a document holding no marker at all took seconds to say so. The YAML reader caps aliases, but validateSettings is a library call that takes a parsed object. gatherUnkeyed now returns whether a value is known to hold no marker and remembers each such mapping or list, to skip it when another path meets it again, as the neighbouring walk in validate.ts does. A subtree that holds a marker is still walked per path, so every path keeps its own warning. A container on a cycle is never remembered: the descent ends where the cycle closes, so what lies beyond the cut is not known, and a document whose cycle the shapes drop (an own __proto__ key) still validates.


COMPAT(v4), and each warning says the next major version refuses the input. Taking either earlier is your decision.Before
After
How
separateRemovalsalso gathers every_removeon an entry no list keys.validateSettingsDocprints both warnings, once the document has no issue.foldLayersprints the warnings only once it stands.separateRemovalsowns the walk, zod'stoDotPaththe spelling.Proof
Technical details
Why a warning, not a refusal
validateSettingsDoccarryCOMPAT(v4)markers, andbun run check:compatlists them.The class: every list of mappings no key holds
_removein the 19 and the 6."_remove":trueand the mock answered 2xx.repository.custom_listand an unknown key of amilestonesentry.reviewersis a nested keyed list (bytypeandid), so it is not in the class._remove: falseand_remove: nullon such an entry draw the warning too: the bundle sends both as a field, so the text is true of them.The census script and its output
Save as
census.mtsin the checkout, thenbun run build:schema && bun census.mts.The walk
toDotPath, as an issue's path is, so a key that only looks like a list step ("rules[0]") is quoted and never renumbered.The layered path
admit.tsnever judged this marker: a layer's unkeyed list replaces the lower one whole, marker included.validateLayerpassesview.asWrittentovalidateSettingsDocthrough one newValidateOptionsmember.foldLayersholds every validation's lines incapturingIoand replays them in order once the fold stands.Public surface
ValidateOptionsinsrc/engine/orchestrate.tsis the engine's own. The library's type of that name is declared insrc/flows/library.ts.bun run build:libon main and on this branch gives an identicallib/pkg/index.d.ts, the shared chunk's hashed file name aside.foldLayers. Onlylib/pkg/internal.d.tsexportsvalidateSettingsDocandfoldLayers.Tests
bun test test/engine/orchestrate.test.ts test/flows/layers.test.ts test/engine/separate-removals.test.tsruns 83 tests here. The first two files run 58 on main, and the third is new.test/engine/orchestrate.test.ts(17): the_layeringwarning (a string, null), its legal-value control, the pinned_removewarning, eight more rows (six other declared lists, a passthrough list, a wrapper's entry), two refused documents that draw no warning, a key spelled like a list step, a wrapper's entry in a layer's view, and a cyclic value control.test/flows/layers.test.ts(5): the layer whose keyed removal precedes the marked entry printsrulesets[1], with the warnings in layer order, and four refused folds print none.test/engine/separate-removals.test.ts(3): the shared document's visit count, a cycle cut short, and a shared list with a marker.render-remove-entryandrender-file-directive(bun test/e2e/run.ts --scenario <name>), and "a removal entry under %s is refused by its site".Scenarios
test/e2e/scenarios/{apply-layering-outside-values-warns,apply-unkeyed-remove-warns,render-unkeyed-remove-warns}.ymlpin each warning line beside the result main also gives.settings_raw). As asettingsmapping it failstest/schema-corpus.test.tswith "schema REJECTS but runtime accepts": the published schema's enum refuses the value the run accepts until the next major.Page
docs/operate/layering.md, the paragraph opening "Inside a keyed entry": one sentence added.Recorded, not built
_removeon a mapping that is no list entry (repository: {_remove: true}) still passes through unannounced by this walk.src/problem.ts, which already holds two copies.validateLayerstill renumbers its issues itself. Only the warnings' paths go through the new option.__proto__).docs/reference/architecture.md("One downgrade"),docs/schema.docs.yml(SettingsFile._layering), and the rendered-file bullet "No_removeanywhere" indocs/operate/layering.md, which a marker in an unkeyed list contradicts on main too.Files
src/engine/{separate-removals.ts,directives.ts,orchestrate.ts,admit.ts},src/flows/layers.tstest/engine/{orchestrate.test.ts,separate-removals.test.ts},test/flows/layers.test.ts,test/e2e/scenarios/{apply-layering-outside-values-warns,apply-unkeyed-remove-warns,render-unkeyed-remove-warns}.ymldocs/operate/layering.md