Repository navigation
A name collision that resolves per-entry and refuses whole-corpus: a lens for the check the gate can make and the author cannot - #9239
Merged
Conversation
added 2 commits
August 25, 2026 21:43
…ule declares already have a declarer elsewhere
…st forked standing were non-exhaustive
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.
Independent of my emission-ratchet stack. Branched off main, and it exists because of a defect that stack hit.
The class
Two modules declaring one bare name is not an error at either declaration. It becomes an error at a call site, and only under a closure containing both declarers.
gunbc compile --entry <caller>contains one declarer → the call resolves, the entry reports 0 blocking.So the class is invisible to the check an author naturally runs and visible only to the one that gates. The cheap check cannot see it; the check that can costs a CI round trip.
Measured specimen (2026-08-25, #9213, run 32896104450):
dag/tools/frontier_ingestion_probedeclaresroster_identity; a second module declared the same name over a different subject returning a different type. Per-entry compile: clean. Floor:function 'roster_identity' not found in scope.And the diagnostic does not say what happened — two declarers and zero declarers arrive as the same message, which is state-space conflation: different states, opposite remedies (rename one of two authorities / write the missing one), rendered identically. That is why the lens reports the peer declarers rather than the bare fact of a fork.
What this is
gunbc.bare_name_fork_lens— a pure reader overdecl_facts, storing nothing — plus one entry point:Scoped to one subject module: do the names this module declares already have a declarer elsewhere. Deliberately not a corpus-wide collision census — that is a different artifact with a different cost, and a count would not be the valuable part anyway.
Three properties that keep it from being a decoration
An unread subject refuses. If the subject path contributes no declarations — mistyped path, file outside the roots, module the index could not classify — the verdict is
SubjectContributedNoDeclarations, not clean. A lens whose only failure mode is found something reads clean whenever it could not read at all: the strongest verdict from the weakest observation, and the one shape that would make this worse than absent.The coverage bound is declared and printed on the clean arm.
decl_factscarries top-level declarations. A coproduct's variant arms are bare names that can also fork, and they are not in this population. So a passing run states what it did not check, with the next-rung trigger named — variant constructors carried as their own facts, at which point the bound is deleted rather than widened here.The pure core reads a projection, not
DeclFact.DeclFactcarries aNode, so every fixture over it would have to mint a syntax node in order to say something about a name — a fixture that expensive is one nobody writes, which is how a lens ends up with a host call and no evidence. The host boundary projects once; the nine claims below it are hermetic and authored.Two constraints recorded in the carrier, not here
Both are stated in the
.dagsource rather than the PR body, since a PR body does not survive the merge.(a) It is not wired into floor witness discovery, and the entry point says why: two corpus-wide censuses were deleted from there in August for exactly this cost shape — the unit of computation was the whole corpus, the unit of fact was one module's declarations, and every consumer that wanted a witness roster paid for a whole-corpus module graph.
(b) It is the interim, not the terminal. The terminal answer is that a declaration fact belongs on the module's own declaration, checked at ingestion where the module is parsed anyway. This lens is the corpus-wide reconstruction that trigger exists to retire, and saying so here is what stops it being cited later as the finished thing.
Evidence
Both entries compile with 0 blocking; all nine claims execute, joined declared-against-PASS.