Repository navigation
fix(engine): keep a lower layer's rename when a higher layer adds to the label by its new name - #597
Conversation
…the label by its new name
Under deep, a higher entry {name: defect, description: a defect} over a lower {name: bug, new_name: defect} pairs with it through the rename target, and the field-by-field merge let the higher name win. The folded entry renamed "defect" to itself and no longer claimed "bug", so a plan against a repository that holds "bug" deleted it as undeclared and created "defect", which takes the label off every issue.
A deep pair is now written under the higher entry's key unless that drops a key the lower entry claimed and the lower entry's key keeps every key the pair claimed. The stack above folds to {name: bug, new_name: defect, description: a defect} and plans one update. A higher entry that itself renames to a different name stands as written, and shallow still swaps the entry as written. Only labels claims two keys per entry, so no other keyed list changes.
The fuzz oracle models the fold independently, so its own statement of the rule moves with the engine's, and the row that pins both folds is re-expected with the engine's row.
docs/operate/layering.md states the rule and what shallow does with the same layers, says an environment's reviewers keep their written order in the rendered file (the canonical order never sorted them), and gains the refusal row for a ruleset that lists a bypass actor twice.
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 check1 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.
🟢 Approval recommended
No active review issues remain for the change preserving label renames across deep layers.
No open issues 🎉
What changed in this PR
This PR preserves a lower layer's label rename when a higher layer adds fields using the new name. Under deep layering, this keeps apply from deleting and recreating the label, preserving its issue assignments.
Changes:
- Preserve the original label name when needed to retain a paired entry's claimed keys, while respecting higher-layer renames.
- Update the independent fold oracle and add regression coverage asserting one update instead of a delete and create.
- Document deep versus shallow rename behavior, correct reviewer ordering guidance, and add a duplicate bypass-actor refusal example.
| File | Description |
|---|---|
test/engine/layers.test.ts |
Tests preserved renames against live labels and higher-layer rename precedence. |
test/e2e/generators/oracle.ts |
Models key-preserving deep merges independently. |
test/e2e/generators/oracle.test.ts |
Updates the expected fold to retain the original label name. |
src/engine/layers.ts |
Preserves claimed keys when merging one-to-one deep pairs. |
docs/operate/layering.md |
Explains rename behavior, reviewer ordering, and duplicate actor validation. |
Review effort: Balanced
…hich keyed lists keep their order
The page stated the exception to the rename rule by spelling, while the fold decides on folded keys: over {name: bug, new_name: defect}, a higher {name: defect, new_name: Defect} claims one name and renders {name: bug, new_name: Defect} under deep. The rule paragraph now says when the lower name is written, the paragraph after the example gives that stack and the rename on to a third name, and the spelling sentence names its one exception.
The canonical-order list and the sentence under it said every keyed list is sorted, directly above the lists kept as written. Both now except those lists.
The comment above mergedPair and the one above the fuzz oracle's helper keep the consequence for the planner and drop the selection the code shows. The oracle's helper is renamed pairKeepingNamesWhenItCan, since its fallback keeps the higher layer's key when no key field keeps every name.
…e rule Three statements on the page still read as having no exception: the scalar row and the deep row of the rules table, and the sentence that opens the null section. Each now carries one clause naming a label's name and the rename rule. The null section's lead-in sentence moves to its own line, so the paragraph stays at 70 words, and the formatter re-pads the other rows of the table without changing their words.
Before
After
For the owner: this re-expects a row
test/engine/layers.test.tspinned on purpose, the self-rename{name: defect, new_name: defect}. Planned against a repository that holdsbug, that shape deletes the label from every issue, sincelabelsdeletes undeclared entries by default.How
labelsclaims two keys per entry.Proof
Technical details
The captured blocks
bun run build:bundleand runs it withINPUT_MODE=renderover the two layer files.parseScenarioandrunScenario, withlive_state.labelsholdingbug.origin/mainat faf72da, After is this branch.-- writesthe mock's request log without its GETs.fleet.ymlalone renders{name: bug, new_name: defect}and applies onePATCH /repos/{repo}/labels/bug, on both trees.Tests
bun test test/engine/layers.test.ts -t "still renames the live label"on the unfixed fold gavedeleting undeclared label "bug"andcreating label "defect"where it expectsupdating label "defect".bun test test/engine test/e2e/generators/oracle.test.ts test/docsgives 930 pass, 0 fail.[{name: defect, new_name: defect, color: ffffff}]and now expects[{name: bug, new_name: defect, color: ffffff}].deepdirective (feat(engine)!: layer every list section by key with replace, shallow, and deep #287) in a table that asserts one label the planner creates once over an empty repository, and that assertion still runs for it.FOLD_CASESrow "a label renaming into a name a higher entry declares is one resource" expected{name: Defect, new_name: defect, color: "222222"}and now expects{name: bug, new_name: defect, color: "222222"}.test/e2e/generators/oracle.tsmodels the fold in its own code, and that row runs both folds, so the two change together.{name: defect, new_name: flaw}over{name: bug, new_name: defect}stays{name: defect, new_name: flaw}, which pins the guard.Fuzz
FUZZ_SEED=29 bun test/e2e/fuzz.ts --iterations 25onorigin/mainplus this branch'ssrc/engine/layers.tsalone printsmerge/valid/deep seed 2945009622 FAILand exits 1.merge/valid/deep okand25/25 iterations ok, and exits 0.origin/main.Reach, per keyed list
labelsnew_nameandname)name, as shown aboveOther stacks whose result changes
{bug -> defect}stands for{name: bug, new_name: defect}, and<reads "below".main{bug -> defect}<{name: defect, new_name: defect}{name: defect, new_name: defect}: deletesbug, createsdefect{name: bug, new_name: defect}: one update{bug -> defect}<{name: defect, new_name: Defect}{name: defect, new_name: Defect}: deletesbug, createsDefect{name: bug, new_name: Defect}: one update{bug -> defect}<{name: defect, description: d}<{name: bug, _remove: true}labels[0] carries _remove: true, but no lower layer declares an entry under its key{bug -> defect}<{name: defect, description: d}<{name: bug, new_name: flaw}{name: defect, new_name: defect, description: d}and{name: bug, new_name: flaw}{name: bug, new_name: flaw, description: d}{bug -> defect}<{name: defect, new_name: flaw}<{name: flaw, color: ffffff}{name: flaw, new_name: flaw, color: ffffff}{name: defect, new_name: flaw, color: ffffff}{bug -> defect}<{name: defect, description: d}<{name: bug, color: ffffff}{name: defect, new_name: defect, description: d}and{name: bug, color: ffffff}: createsdefect, updatesbug{name: bug, new_name: defect, description: d, color: ffffff}: one update{bug -> defect}<{name: defect, description: d}<{name: bug, new_name: bug}{name: defect, new_name: defect, description: d}and{name: bug, new_name: bug}: createsdefect{name: bug, new_name: bug, description: d}: one update{bug -> defect}<{name: defect, description: d}<{name: flaw, new_name: bug}{name: defect, new_name: defect, description: d}and{name: flaw, new_name: bug}: createsdefect{name: flaw, new_name: bug, description: d}: one updatedeepwithmergeLayerson both trees and plans the result against a repository that holdsbug.bug(the sixth row):mainended with two labels,defectcreated besidebug, and this head ends with one,bugrenamed todefect.{name: flaw, color: ffffff}row, a repository that holdsdefect:maindeletesdefectand createsflaw, and this head plans one update.{bug -> defect}<{name: defect, new_name: flaw}renders{name: defect, new_name: flaw}on both revisions, so a repository that still holdsbuggets a delete and a create.{name: Defect, color: ffffff}over{bug -> defect}renderedname: Defectonmainand rendersname: bughere, and the page's spelling sentence now states that exception.Why that table is the whole list
bug,defect,Defect, andflawon both trees, underdeepand undershallow.shallowdiffers in 0 anddeepin 1,388.name.{name: bug, new_name: flaw}row, and the last three rows.OUTSIDE THE TABLE, and it prints no such line.enumerate.mts
Other pages that state the old rule, left for a docs follow-up
docs/reference/architecture.md:171anddocs/upgrading/v2-to-v3/layering.md:180: both say the higher layer's value wins at every depth, with no exception for a label'sname.The rename test table's other rows
The two docs items
reviewersis not kept as written, whileLISTS_AS_WRITTENinsrc/engine/canonical.tskeeps it.test/e2e/scenarios/render-environments-nested.yml, which pins reviewers in union order, so the page is the side corrected.test/docs/guides.test.tsreproduces like every row of that table.Not changed
layering: shallowrender{name: defect, description: a defect}and apply a DELETE ofbugand a POST ofdefect, as onmain.src/sections/changes, and the fold still reads a section only throughkeys()andkeyField.