feat(Builder): a merger registry, and one entry per key in the Specification - #2220
Open
DerManoMann wants to merge 3 commits into
Open
DerManoMann wants to merge 3 commits into
DerManoMann wants to merge 3 commits into
Conversation
DerManoMann
force-pushed
the
feat/merge-pass
branch
from
September 28, 2026 06:13
93ef782 to
ac28b89
Compare
`setLogger()` via `LoggerAwareInterface`, so a logger can be handed over after the pipeline was built — the builder has the build's logger only by then, and a `setLogger()` call after `withAugmenters()` used to leave the pipeline on the null logger. `process()` takes an optional callable run after every pipe, on what that pipe left. A caller that has to know what each pipe added cannot wait until the end to ask, because the pipe reading the answer may be in the pipeline itself.
An attribute declared in source carries a reflector and needs nothing more. One produced by anything else had no answer at all, and reported its location as the word `unknown`. `Spec\Origin` gives it one: a `producer` label naming the kind of step that added it — `assembler`, `contribution`, `resolver`, `augmenter`, `hybrid bridge` — which `getSourceLocation()` reports where there is no reflector. Beside it is an open bag, keyed by whoever writes to it, that the pipeline never reads. `Builder::stamp()` applies a label after each step that can add, and each pass leaves nothing unlabelled, so an attribute without one is what that step just added. Only reflector-less attributes are stamped, and a label already set is kept: these are defaults, and anything adding to the specification is free to say something more exact. `Origin` is immutable, because `Augmenter\PathItems` and `Augmenter\Inheritance` clone attributes and a clone taking a stamp must not rewrite its original's.
…ication Two attributes claiming one key reached the compiler together, which wrote both into a PHP array and kept whichever it wrote last. Nothing said so outside the component buckets, and it was not even one rule: path items folded with `+`, so for those the *first* won. `Augmenter\Merge` decides instead. It groups each root collection on the claiming merger's `identity()`, folds each group in producer order and writes one entry per key back, so the compiler never sees a collision. It runs first in the reduce phase, the earliest point every identity exists, and again as the last default pipe, for what a late augmenter added. `Merge\LastWins` is the catch-all: it claims every type, keys each collection the way the document does — component key, path and method, webhook and method, path, tag name — and on a collision keeps the later entry and warns naming both halves. Positional lists have no key and pass through. `Builder::withMergers()` configures the chain, the fourth default the builder holds through a hook of its own. A spec build now collects what its pipeline logs, so a merger's warning reaches `Result`; the compiler keeps its own logger and nothing is collected twice. One output change: a `PathItem` against a `PathItem` for one path moves from first-wins to last-wins, the same rule as everything else. A collision inside one attribute is left alone: those halves were written in one place by one author, and the compiler keeps the last of them as it always has.
DerManoMann
force-pushed
the
feat/merge-pass
branch
from
September 30, 2026 00:05
ac28b89 to
10a431b
Compare
DerManoMann
marked this pull request as ready for review
September 30, 2026 00:19
This branch has not been deployed
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.
Depends on #2219, and includes its commits until that merges — review the last three only, or wait and I will rebase.
Overview
Two attributes can claim one key. Two operations on the same path and method, two schemas named
Pet— a scan finds one, awithSpecification()hook contributes the other, an inheritance clone makes a third. Nothing decided between them, so the compiler decided by accident: it writes each into a PHP array and keeps whichever it wrote last, silently. Classic reports the same collision as an error.It was not even one rule.
compilePaths()writes operations with=and folds path items in with+, so the last operation for a path and method won while the first path item for a path did. One map, one build, opposite rules, nothing reported either way. Webhooks were the quietest case: same last-wins, and no diagnostic of any kind, since a webhook operation gets nooperationIdand so never trips the accidental uniqueness warning that operations happen to have.This gives the decision an owner. A merger says what makes two attributes the same one and which survives; the pass applies the chain to the
Specification's own collections and writes one entry per key back, so what reaches the compiler has no collisions left and the compiler's incidental rules stop deciding anything. The shipped merger keeps the later entry and says so, which is what was happening anyway — now stated once, for every collection with a key, and reported.Only the root collections. That is where the halves come from different places and something has to choose. Two entries inside one attribute were written in one place by one author, so the compiler keeps the last of them as it always has.
Warnings a spec build produced were also being dropped:
Resultcarried the compiler's diagnostics and nothing the pipeline said, so a merger's report would have gone nowhere.Changes
Contracts\MergerInterface—supports(),identity(),merge(); a chain, first match wins, the shapeResolverInterfacehasAugmenter\Merge— the pass, first in the reduce phase and again as the last default pipeMerge\LastWins— the catch-all, keying each root collection the way the document does and reporting a collision with both locationsBuilder::withMergers()andgetDefaultMergers(), ordered byUtils\TypedListlike the augmentersSpec\Originon every attribute, carrying what produced it plus an open bag the pipeline never reads;getSourceLocation()reports the label where there is no reflector, in place ofunknownUtils\Pipelinetakes a logger after construction and runs an optional hook after each pipeBuildercollects what the spec pipeline logs intoResultcompilePaths()documents what its union still decides now that duplicate path items are goneBuilderreference; the reference pages list the shipped merger and the new pipeOne output change: a
PathItemagainst aPathItemfor one path moves from first-wins to last-wins, the same rule as everything else. Nothing can have relied on it, since neither behaviour was documented or reported.