Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7e4c842
test(lspcheck): red-first arms for the tower order, the disclosure, p…
mpapis Oct 20, 2026
05dee3c
refactor(model): hoist the dedup specificity ladder to kindSpecificit…
mpapis Oct 20, 2026
ffa6c06
feat(graph): order every multi-def name answer with one tower
mpapis Oct 20, 2026
e5d39f7
docs(resolvers): say "tower front" where the code now means it
mpapis Oct 20, 2026
9b0817f
feat(lsp): disclose multi-definition names with one sentence
mpapis Oct 20, 2026
e633ff4
test(lspcheck): schema-column click arm and known-gap coherence arm
mpapis Oct 20, 2026
17314bb
refactor(lsp,model): clear the quality-delta gating rows C opened
mpapis Oct 20, 2026
33c3682
chore(quality): ack the disclosure-shape rows C leaves, with reason
mpapis Oct 20, 2026
496ad92
review-fix iteration 1: legend truths, seed/union scoping, a (21) tha…
mpapis Oct 21, 2026
aae42d3
review-fix iteration 2: edit-check focus semantics, reorder-proof lad…
mpapis Oct 21, 2026
bedff61
review-fix iteration 3: comment truth, first-row anchors, a claim mat…
mpapis Oct 21, 2026
96d8ae4
review-fix iteration 4: the emitted sentence, discarded-sort claims, …
mpapis Oct 22, 2026
75df8eb
review-fix iteration 5: equivalence claims checked against the code t…
mpapis Oct 22, 2026
467bbad
Merge origin/main (train 33) into #432
joyful-ii-V-I Oct 24, 2026
f5d18ae
tower: a constructor ranks below its class; the multi-def sentence no…
joyful-ii-V-I Oct 24, 2026
97d0b6d
changelog.d: #432 fragment, crediting @mpapis
joyful-ii-V-I Oct 24, 2026
d9fe1e7
lspcheck: arm the tower's tier order; resolve WORK so URI pins hold o…
joyful-ii-V-I Oct 24, 2026
c1a9973
printf_parity.manifest: re-pin around and safe_delete for the tower l…
joyful-ii-V-I Oct 24, 2026
a6a3afe
Merge origin/main (train 34) into #432
joyful-ii-V-I Oct 25, 2026
0cd08b6
lego: read the tower by role — extracted contract, then implementors,…
joyful-ii-V-I Oct 25, 2026
e59e5d8
lego: implementors outrank the extracted contract on the type rung (r…
joyful-ii-V-I Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .ripwire_quality_acks

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions changelog.d/432-name-tower.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Changed — one order for every multi-definition name answer, and `--lsp` says when a name has several definitions (#432, thanks @mpapis)

- When a bare name has several definitions, every verb now orders them the same way: the most specific kind first (a method before a variable before a doc heading or config key; a constructor ranks just below its own class), then the leading definition's language, then crawl order. Single-pick verbs (`--around`, `--lego`, `--connect`'s preferred definition, `--impact`'s focus, `--owners`, `--edit-check`) answer at the front of that order; union verbs (`--callers`, `--uses`, `--impact`'s `of_def=` rows, `--lsp` definition/workspace-symbol lists) keep every definition and only reorder it. Train 33's code-first rule and the C/C++ declaration-to-body rule still apply on top. `defs=` still says how many definitions the name has, so a pick among several stays disclosed (thanks @mpapis, #432).
- `--lego` reads the order by role: it asks for a type, so a class, struct, interface or named type all share one rung. On that rung a type with at least one implementor comes first, then a type whose method contract is read (`<m>` rows, no `not-extracted-for-lang` caveat; today that is C++ and Objective-C, the languages whose `<m>` rows are read), then the order above. So `--lego=Shape` answers the C++ `Shape` with its contract, not an Elixir module that has none, and a Kotlin `interface` (tagged as a class) that has an implementor is not passed over for a same-named Java one that has none (diagnosis @mpapis, #432).
- `--lsp` definition sends one `window/logMessage` before the list, and hover adds one sentence, when the name has more than one definition. This is a count of same-named definitions, prototypes included. It does not mean the call graph split a call between them.
- The map is unchanged: the order applies at query time only, so call-edge candidates, `kParserVer` and caches do not move.
10 changes: 6 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -747,9 +747,11 @@ definitions and says how it got there. Input spellings: a bare name, `file:name`
the canonical id the tool prints, `@FILE:LINE`, a `*` glob, and the changed symbols of a diff. All of them go through
`resolveAllByNameQualified` (`graph.h`) or one of its tiers. The diff-hunk producer (`diffhunks.h`, `situ.h`) maps
hunks to the symbols whose bodies they touch, and falls back to the whole file when no body holds a hunk
(contributed by @llvm-x86 in #423). Matches come back in index order (ascending id); a verb that needs ONE
focus takes it from `resolveFocus` (`graph.h`): a code definition before a docs heading or a config key, the lowest id
among those, and a bodyless C/C++ declaration yields to the same-scope definition with a body. A seed resolver never
(contributed by @llvm-x86 in #423). Several definitions with one name are ordered by one total order, the name tower
(`applyNameTower`, `kindSpecificity` in `model.h`; contributed by @mpapis in #432). A verb that needs ONE focus takes
it from `resolveFocus` (`graph.h`; `--lego` and the MCP `lego` verb take theirs from `resolveLegoFocus`: the
role order, then `focusOfOrderedMatches`): the tower front, a code definition before a docs heading or a config
key, and a bodyless C/C++ declaration yields to the same-scope definition with a body. A seed resolver never
picks silently. `defs=`, `unproven_defs=`, `from_defs=`/`to_defs=` and the `<def>` table disclose the candidates.
The rule: **any spelling the output prints is a spelling the input accepts.**

Expand Down Expand Up @@ -782,7 +784,7 @@ cut something is printed (`capped=`, `total=`, `next=`). It is never reported as
**One coupling, stated on purpose.** When a name has several definitions, the *pick* can depend on the walk:
the right `Loop` is the one that reaches the other endpoint. The seed layer therefore returns the ordered
candidates. A walk that must choose uses the one shared terminal scorer (`scoreTerminalCandidates`, `graph.h`),
with `resolveFocus`'s order as the tie-break, and it prints the alternatives it did not take.
with the tower as the tie-break, and it prints the alternatives it did not take.

**Adding to a layer.**
- A new spelling goes in the seed resolver. The verbs pick it up from there.
Expand Down
8 changes: 4 additions & 4 deletions src/compactlegend.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,14 @@ inline constexpr CompactLegendSpec kCompactLegendSpecs[] =
{ "edit-check", "edit-check", "sym='s contract NOW vs HEAD: status=unchanged|new-symbol|contract-change; <c n= p= incompatible=1 sites_l=> callers" },
// safe-delete (2026-09-12, the fourth sweep): radius_tested=/radius_untested= ride every answer and read here, by the same
// isTestedByReach lens as --impact's pair (verbs_navigate.h runSafeDelete), over impact_reaches= instead of reaches=. The
// sweep's last pass read the rest of that root against runSafeDelete: t= and p= are defs[0]'s, the lowest id
// sweep's last pass read the rest of that root against runSafeDelete: t= and p= are defs[0]'s, the tower-front match
// resolveAllByNameQualified returns. That is a MATCH, not always a definition (the sweep's design review): on a
// header-qualified file:name selector the decl→def widening keeps the declarations beside the definitions it adds
// (graph.h declToDefFollowThrough), and a declaration can hold the lowest id, so the reading names the unit defs= counts.
// (graph.h declToDefFollowThrough), and a declaration can hold the front, so the reading names the unit defs= counts.
// ambiguous_callers= counts callers whose g.ambOut is non-zero; dead_code_candidate=1 needs defs=1, no 1-hop caller,
// deadCodeEligibleKind (SymKind::Function with a body, not in a .h/.hpp/.hh/.hxx path) and a whole-word `static` inside the
// signature span (quality.h sourceHasStaticToken).
{ "safe-delete", "safe-delete", "can sym= go, a READ never a verdict: callers= impact_reaches= uses= tested_self= risk=; <c n= p= amb=>; radius_tested= non-tests of impact_reaches= an indexed test reaches, radius_untested= the rest; t= p= its lowest-id match's kind and file:line; defs= matched; ambiguous_callers= callers with a call split over several defs; dead_code_candidate=1 only at defs=1 with no caller, for a t=fn with a body, outside .h/.hpp/.hh/.hxx, whose signature spells static (0 never means in use)" },
{ "safe-delete", "safe-delete", "can sym= go, a READ never a verdict: callers= impact_reaches= uses= tested_self= risk=; <c n= p= amb=>; radius_tested= non-tests of impact_reaches= an indexed test reaches, radius_untested= the rest; t= p= its tower-front match's kind and file:line; defs= matched; ambiguous_callers= callers with a call split over several defs; dead_code_candidate=1 only at defs=1 with no caller, for a t=fn with a body, outside .h/.hpp/.hh/.hxx, whose signature spells static (0 never means in use)" },
{ "quality-delta","quality-delta","only what the change made WORSE vs baseline=: regressions= minor= gating=; <r kind= sym= p= was= now= gating= bar=>, <sa> acked" },
{ "test-gate", "test-gate", "tests <t p= changed= partner= hops= run=> + untested blast radius <u sym= p= l= ccx=>; exit 4 while either exists" },
{ "affected", "affected", "test files that transitively reach the changed files/symbols: <test p= partner= hops= run=> in evidence order (order= partners=); seeded_by= the reading taken" },
Expand Down Expand Up @@ -466,7 +466,7 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] =
// defs= names resolveFocus's pick (graph.h), and the body preference there holds only IN THE DECLARATION'S SCOPE: a pure
// virtual beside another class's override keeps the focus. Unconditional, this reading described that answer wrongly
// (test/decltodefcheck.sh E3g, which also holds the full --around and --connect readings to the same condition).
{ "defs", "defs=N: of= names N defs; lowest-id walked, code first, a C/C++ body in the same scope over its declaration", true, "r" },
{ "defs", "defs=N: of= names N defs; most-specific kind walked first, its language ahead, id last, code first, a C/C++ body in the same scope over its declaration", true, "r" },
// The map HEADER's absent-at-zero gauges: `<!-- files=` is kept as data while the `<!-- hdr:` clauses that define
// these fields go (kDeclinedMapLegend, kIgnoredLegend, kExtentSuspectHdrLegend, kMacroBlankedHdrLegend, the absent-if-0
// half of the always-on legend, kMaxTokensFitLegend). Header-ONLY: several are quoted attributes elsewhere.
Expand Down
23 changes: 16 additions & 7 deletions src/editcheck.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,20 @@ inline std::vector<NodeId> editCheckOverloadSet( const IngestResult& ing, const
// resolveAllByNameQualified), verified by editCheckRoundTripSpelling.
struct EditCheckGroup
{
NodeId lowestNode; // the lowest-id definition in the group — the focus resolveFocus would have picked
NodeId focusNode; // the group's tower-front member — its orientation node. resolveFocus picks the SAME node
// from these matches EXCEPT where its C/C++ body-yield (graph.h) moves to the first bodied
// same-scope member later in the group: a prototype beside its definition in one file keeps
// the declaration here while the single-pick verbs answer at the definition. No answer is
// wrong — editCheckOverloadSet unifies the contract from either node.
std::string spelling; // a selector that resolves to THIS group and no other (when `unique`)
bool declOnly; // only C/C++ declarations, beside a group that holds a definition: never offered (M2)
bool unique; // `spelling` was verified to resolve to this group alone
};

// The (file, contract) KEYS of a match list with each key's members, in first-seen order: matches arrive in ascending
// node id (resolveAllByNameQualified walks ing.symbols in order), so members[i][0] IS group i's lowest id.
// The (file, contract) KEYS of a match list with each key's members, in first-seen order: the name/file tier of
// resolveAllByNameQualified comes back TOWER-ordered (applyNameTower), its early canonical-id/Scope::name tiers ascend by
// id, so members[i][0] is group i's tower front on the name path — a POSITION, not a guarantee of the lowest id, which is
// why editCheckRoundTripSpelling compares membership rather than first elements.
struct EditCheckGroupKeys
{
std::vector<std::pair<std::uint32_t, std::string>> keys;
Expand Down Expand Up @@ -286,13 +292,16 @@ inline EditCheckGroupKeys editCheckGroupKeys( const IngestResult& ing, const Gra
// the declaration AND the definition and was refused again. So each shown spelling is re-resolved here and must land on
// exactly this group; otherwise the canonical id, then the `@FILE:LINE` seed (one place, one symbol) is tried. A
// cwd-spelled path the file tier would read as a substring of another (`./lib.cpp` inside `./sub/lib.cpp`) now names
// its own file exactly (graph.h preferExactFile), so it round-trips. When nothing does — a line holding two
// its own file exactly (graph.h preferExactFile), so it round-trips. The comparison is ORDER-INDEPENDENT on purpose: the
// tiers re-resolve in different orders (canonical id ascends by id, the name tier is tower-ordered), so the proof that a
// spelling lands on THIS group is that the one rebuilt group CONTAINS the focus node — its (file, contract) key is derived
// from that node, so containment pins the group whatever the arrival order. When nothing does — a line holding two
// definitions — `unique` stays false and the refusal SAYS so beside that spelling, never offering it as the example.
constexpr std::size_t kEditCheckSpellingsShown = 6;

inline void editCheckRoundTripSpelling( const IngestResult& ing, const Graph& g, EditCheckGroup& group, const std::string& canon, bool foldDecls )
{
const Symbol& s = ing.symbols[ group.lowestNode ];
const Symbol& s = ing.symbols[ group.focusNode ];
std::vector<std::string> tries{ group.spelling };
if( canon != s.name && canon != group.spelling )
{
Expand All @@ -303,7 +312,7 @@ inline void editCheckRoundTripSpelling( const IngestResult& ing, const Graph& g,
{
const std::vector<NodeId> again = resolveAllByNameQualified( ing, spelling );
const EditCheckGroupKeys gk = editCheckGroupKeys( ing, g, again, foldDecls );
if( gk.keys.size() == 1 && gk.members[0].front() == group.lowestNode )
if( gk.keys.size() == 1 && std::find( gk.members[ 0 ].begin(), gk.members[ 0 ].end(), group.focusNode ) != gk.members[ 0 ].end() )
{
group.spelling = spelling;
group.unique = true;
Expand Down Expand Up @@ -331,7 +340,7 @@ inline std::vector<EditCheckGroup> editCheckGroups( const IngestResult& ing, con
// the canonical id has degraded to the bare name (a scope-less definition), which disambiguates nothing.
for( std::size_t groupIndex = 0; groupIndex < groups.size(); ++groupIndex )
{
const Symbol& s = ing.symbols[ groups[ groupIndex ].lowestNode ];
const Symbol& s = ing.symbols[ groups[ groupIndex ].focusNode ];
const std::string& canonOfThis = gk.keys[ groupIndex ].second;
std::size_t groupsInFile = 0;
for( const auto& [fileId, canon] : gk.keys )
Expand Down
2 changes: 1 addition & 1 deletion src/editpreview.h
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ inline Outcome run( const IngestResult& ing, const Graph& g, const std::string&
// 2026-09-10: the same window the post-hoc verb takes, from the same flags — a preview that paged
// differently from the answer it predicts would be worth nothing (test/editpreviewcheck.sh compares
// the two documents).
oc.xml = editCheckBundleText( merged, mg, root, maxFileBytes, excludes, groups[0].lowestNode, ni, true,
oc.xml = editCheckBundleText( merged, mg, root, maxFileBytes, excludes, groups[0].focusNode, ni, true,
EditCheckPage{ pageLimit, pageOffset }, previewUnprovenDefs, /*notesDegraded=*/false,
EditCheckSpliced{ newBytes, fsym.fileId, true } ); // the merged spans index the spliced bytes
// E3 (terminality round A, 2026-09-05): the CURRENT span an apply would replace, as the bytes are on disk, so
Expand Down
Loading
Loading