Skip to content

fix(doc-links): report a path::member reference to a missing member (#2587) - #2601

Merged
DeusData merged 6 commits into
DeusData:mainfrom
bline:fix/doc-link-missing-member
Oct 29, 2026
Merged

DeusData merged 6 commits into
DeusData:mainfrom
bline:fix/doc-link-missing-member

Conversation

@bline

@bline bline commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #2587. A Markdown reference path::member whose member the code file
does not define became an exact MENTIONS edge to the file. A renamed or
deleted definition therefore looked like a valid, coarser link, and nothing
recorded that the member was not found.

mdr_segment now reports how a member lookup ended, and mdr_resolve_path
uses that:

reference before after
src/util/rounding.py::clamp (defined) edge to clamp unchanged
src/shapes.py::Square::area (owner picks one) edge to Square.area unchanged
src/util/rounding.py::no_such_function exact edge to the file unresolved missing
src/shapes.py::area (two area methods, no owner) exact edge to the file unresolved ambiguous
docs/other.md::Intro (a document's heading) edge to the file unchanged

This follows the C resolver: cr_member reports a missing member of an
existing type as missing and never falls back to an edge on the type. A
missing row is also what a missing path already gets, so index_status's
unresolved counts now show member-level doc rot. Members written on non-code
files keep the file edge, because a document's headings are not graph
definitions. Line ranges without a member are unchanged.

The other suggestion in #2587, keeping the edge and recording the
unresolved member on it, would need an outcome that is both an edge and a row.
That is a new mechanism, so I went with the existing one. Happy to switch.

Evidence

doc_links_md_member_not_defined_issue2587 in tests/test_doc_links_md.c
indexes the fixture in the table. Without the source change it fails on the
defect itself:

FAIL tests/test_doc_links_md.c:429: dm_count(... AND t.file_path = 'src/util/rounding.py') == 1, expected 0

With it, scripts/test.sh --suites doc_links_md,doc_links_rst,doc_links_adoc,doc_mentions
reports 96 passed, 0 failed.

Checklist

  • Every commit is signed off (git commit -s) — required, CI rejects
    unsigned commits (DCO, see CONTRIBUTING.md)
  • Tests pass locally: scripts/test.sh (the canonical leg), 9275 passed, 0 failed, 10 skipped,
    186 suites, Linux x86-64, gcc
  • Lint passes: scripts/lint.sh --ci in test-infrastructure lint container
    (cppcheck + clang-format-20 + memory-core), "All linters passed"
  • New behavior is covered by a test (reproduce-first for bug fixes)

Written with an AI agent (Claude); I reviewed the change and certify it via
the DCO sign-off.

🤖 Generated with Claude Code

…eusData#2587)

A Markdown reference `path::member` whose member the code file does not
define became an exact MENTIONS edge to the file. A renamed or deleted
definition therefore looked like a valid, coarser link instead of a stale
reference, and nothing recorded that the member was not found.

mdr_segment now says how a member lookup ended. When a code file does not
define the member, the reference is an unresolved `missing` row, the same
doc-rot reason a missing path gets. When the file defines the name more
than once and no owner picks one, it is `ambiguous`. This matches the C
resolver, which reports a missing member of an existing type as missing
rather than falling back to the type. A member written on a non-code
file (a document's heading) keeps the file edge, since those are not
graph definitions. Line ranges without a member are unchanged.

Test (tests/test_doc_links_md.c) indexes a fixture with a found member, a
member reached through its owner, a missing member, a name defined twice,
and a member of a Markdown file. Without the fix it fails on the defect:
the section's MENTIONS edge to src/util/rounding.py exists (1, expected 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Scott Beck <296871+bline@users.noreply.github.com>
@bline
bline requested a review from DeusData as a code owner October 12, 2026 02:59
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData DeusData added bug Something isn't working parsing/quality Graph extraction bugs, false positives, missing edges priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. labels Oct 19, 2026
@DeusData

Copy link
Copy Markdown
Owner

Thanks for the clear missing-versus-ambiguous examples and regression coverage. I have read the proposal to use unresolved rows instead of a file fallback; it remains pending review, including the representation choice you called out.

…usData#2587)

A `path::member` reference became a missing row whenever the member
lookup found no definition node, but real documentation names members
the file has without a node the lookup binds: a call form
(`src/util.py::clamp()`), a C macro or a prototype in a header, a
TypeScript namespace, a Python enum member, an annotated class
attribute. Those lost their correct file edge and were counted as doc
rot.

A member now reads without a trailing call form, so `clamp()` binds
`clamp`. When no definition matches, the resolver reads the code file
and calls the member missing only when the file does not write its name
at all (a whole identifier, case-sensitive); a member the file writes
keeps the file edge it had before. A file that cannot be read whole
makes no claim. Ambiguous members are unchanged.

Test: doc_links_md_member_written_issue2587 indexes each shape under its
own heading, and two members that stay missing (Color::BLUE, which the
file writes only as LIGHT_BLUE and a lowercase "blue", and
no_such_function). On the previous head all seven shapes are missing
rows; without the call-form trim the call form is; without the
identifier boundary, or with a case-insensitive match, BLUE takes the
file edge.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…ed file (DeusData#2587)

An unresolved `path::member` row (missing or ambiguous) leads to its
file by no edge, so an incremental run never re-resolved the document
when that file changed. Two edits then left a row where a full index
has an edge: a body edit that writes a missing member's name (no
definition added), and a same-named pair of methods shrinking to one
(the name stays, so no removed name reaches the rows).

The closure now also takes the files whose missing or ambiguous rows
with "::" name a changed or deleted file, matched by base name since a
reference may be written relative to the document or to the root. When
memory runs out, every such row's file is taken.

Test: doc_links_md_member_missing_incremental requires incremental ==
full on the closure-repair route for both edits and for dropping the
name again. Without the new closure rule the "member written" step
diverges; with it limited to missing rows the "group shrinks" step
does.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you @bline for this fix and for the reproduce-first test. You were right that a path::member reference to a member that no longer exists should not pass as a valid file link, and reusing the existing missing and ambiguous rows (the same choice the C resolver makes) keeps the index_status counts honest without inventing a new mechanism. We want to land it, so we pushed three commits on top of your branch (maintainer edits are enabled; please pull before pushing again):

  1. fix(doc-links): a member the code file writes keeps its file edge. Before merging we ran the change against member shapes that real documentation uses, and several became missing although the member is there: a call form (src/util.py::clamp()), C macros and a header prototype (include/limits.h::MAX_ITEMS, ::SQUARE, ::limits_init), a TypeScript namespace (src/ns.ts::Shapes), a Python enum member (src/color.py::Color::RED) and an annotated class attribute (src/model.py::Model::name). The member lookup only binds definition nodes, and these have none it can bind. A trailing call form is now dropped before the lookup, so clamp() binds clamp. When no definition matches, the resolver reads the code file and reports missing only when the file does not write the name at all (as a whole identifier, case-sensitive); otherwise the reference keeps the file edge it had before. If the file cannot be read whole, it makes no claim. The header comment describes the new outcome. The new test doc_links_md_member_written_issue2587 puts each shape under its own heading, next to two members that must stay missing: Color::BLUE (the file only has LIGHT_BLUE and a lowercase "blue") and no_such_function.

  2. fix(incremental): re-resolve documents whose member rows name a changed file. A missing or ambiguous member row has no edge to its file, so an incremental run never re-resolved the document when that file changed. Two edits then left a row where a full index has an edge: a body edit that adds the member's name, and a pair of same-named methods shrinking to one. The incremental closure now also takes the documents whose such rows name a changed or deleted file. The new test doc_links_md_member_missing_incremental checks incremental == full for both cases.

  3. A merge of current main (406f6c7). Two spots conflicted: the header comment, which your fix(doc-links): report references into a moved directory as missing (#2588) #2602 also edited (the merged text keeps both the moved-directory wording and the member wording), and the test file, where both PRs added a test in the same place (both are kept unchanged).

Your ambiguous handling is unchanged. Thanks again, this is a genuinely useful check.

…eusData#2587)

The "group shrinks" step of doc_links_md_member_missing_incremental
removed one of two Python `area` methods and expected the closure-repair
route. Since the Python namespace admission on main (merged into this
branch with origin/main), a Python file whose surface changes always
takes the full route (closure_decline reason=python_namespace_changed),
so the step ran FORCED_FULL on every platform; the Windows leg was simply
the first to report it. Nothing here depends on timing: the planner
compares content hashes.

The group now lives in a Ruby file. Ruby has no doc-link scope, so the
lost method reports no removed name and the document re-resolves only
through the ambiguous `path::member` rule, which this step exists to
bind. (A Rust group was tried too: its scope reports the method as a
removed name, so the step passed with the rule removed.)

Test: the step takes closure repair with closure=2 (shapes.rb and
docs/areas.md). With "ambiguous" dropped from doc_row_member_reason it
stays on closure repair with closure=1 and keeps the stale ambiguous row
where the full index has the Areas -> Square.area edge.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Hi @bline, thank you again for this PR and for your patience while we worked on it. Your fix for missing members and its tests were the right foundation; everything below builds on them.

We pushed one more small commit (62965bc) on top of your branch. It changes only a test:

  1. After main was merged in, the Windows test leg failed in the incremental test doc_links_md_member_missing_incremental, at the "group shrinks" step. The cause is not specific to Windows (it fails the same way on macOS and Linux): main now always rebuilds fully when a Python file's definitions change, because of how Python import namespaces work. The step removed one of two Python "area" methods, so it took the full rebuild instead of the incremental repair it was checking.
  2. The two same-named "area" methods now live in a small Ruby file instead. Ruby still goes through the incremental repair, so the step again checks the rule it exists for: a document whose path::member reference was ambiguous gets resolved again when the group shrinks to one. We confirmed that the step fails if that rule is removed and passes with it.

No production code changed in this commit. Please pull before pushing again, since the branch has new commits on top. Thanks again for the contribution!

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you for your patience, @bline. CI went green on this branch, but this evening's merges on main touched files this pull request also changes, so that green no longer covered today's main. We merged current main into the branch once more (one merge commit, 9fe25c4; your changes are untouched) and will merge once the fresh checks are green. Please pull before pushing again.

@DeusData
DeusData merged commit 3be5a02 into DeusData:main Oct 29, 2026
26 checks passed
@DeusData

Copy link
Copy Markdown
Owner

Merged as 3be5a02 - thank you again, @bline! A path::member reference to a member that no longer exists now shows up as a missing doc link instead of silently passing as a valid file edge, and members the code file really defines (call forms, C macros and prototypes, namespaces, enum members) keep their file edge. Reusing the existing missing and ambiguous rows kept index_status honest without a new mechanism, and your reproduce-first test made each round of review easy. Thank you also for your patience while main kept moving under this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working parsing/quality Graph extraction bugs, false positives, missing edges priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A path::member reference to a missing member silently becomes a file edge

2 participants