package_root_of returns EMPTY when a SRC_DIRS name is the first path component, so src/- and lib/-headed trees can anchor no qualifier — #175's own repro still fails because of it #247

Closed
opened 2026-09-10 02:25:30 +02:00 by buildagent · 2 comments
Member

Found by the #168/#175 lane, and it is why #175's headline repro still does not resolve after #175 was fixed. Two repositories in the pinned corpus hit it, by two different names.

The mechanism

index::package_root_of returns the prefix before the first conventional source directory:

const SRC_DIRS: &[&str] = &["src", "lib", "tests", "test", "app", "spec", "specs", "source"];

When one of those names is the first component, the loop matches at index 0 and returns "". pkg_tail is then empty, temp.pkg_lang skips it (WHERE fp.pkg_tail != ''), and the package can anchor no qualifier at all.

Two measured instances, two repositories

python-flask. Every file under src/flask/ has an empty package root, so flask anchors on nothing. This is why flask.url_for — the worked example in #175's own body — still fails after #175's fix. #175 correctly made the plugin emit a module-qualified call as a qualified call rather than a method_call; the class is fixed, and this repro needs this defect closed too.

rust-analyzer. #168 recorded the same thing under lib/: four real packages live at lib/line-index, lib/la-arena, lib/lsp-server, and lib/line-index/src/lib.rs matches lib at index 0.

So it is not a Python quirk or a Rust quirk — it is the rule, and any repository that puts packages under a top-level directory whose name happens to be in that list loses qualifier anchoring for all of them.

Why the two directions #168 named were refused, and what changed

#168 proposed dropping lib from SRC_DIRS, or preferring the manifest. Both were measured and refused there. But #168 has since been fixed by a different shape — qual_root_scope_sql, which gates a stem-anchored candidate on the qualifier's root — and that fix does not touch package_root_of. So this half is now cleanly separable in a way it was not when #168 was filed:

  • package_root_of is about which package a file belongs to.
  • qual_root_scope_sql is about which package a qualifier may reach into.

They were entangled while both were broken. Only the first is left.

Shape of a fix, from #168's own direction 2

manifest_package_dirs / nearest_package_dir already know that lib/line-index holds a Cargo.toml and that src/flask sits under a pyproject.toml. pkg_tail is still derived from the lexical package_root_of. Deriving the tail from pkg_dir when a manifest exists fixes this class without touching the lexical fallback where no manifest does — the same "manifests do not guess" argument that settled #134.

The narrower alternative, also from #168: accept a SRC_DIRS name only when it is not the first component. lib/ as a package container and mypkg/lib/ as a source directory are different things, and only the second is what the list is for.

What must NOT be done

  • Do not just drop lib (or src) from the list. Every Ruby and PHP layout that uses lib/ as a genuine source root depends on it — measurable on ruby-sinatra and php-guzzle, and that is exactly why #168 refused this.
  • Do not fix it by widening the anchor. #168 measured what happens: containment alone withdrew 35 correct binds on rust-analyzer. The anchor is not the broken part.
  • Do not measure success by the resolved count. #175's own close blessed a −16 on python-flask that was pure phantom removal. Adjudicate binds at source, joined on (path, line, col, kind, occurrence) against the prior binary.

What a fix must prove

  • flask.url_for in python-flask resolves to src/flask/helpers.py:200 — not to src/flask/app.py:1102, which is the Flask method and was the phantom #175 removed. A repro that resolves to the wrong one is worse than one that resolves to nothing.
  • lib/line-index in rust-analyzer gets a non-empty package tail.
  • Anti-vacuity, and this is the arm that decides it: ruby-sinatra and php-guzzle, whose lib/ genuinely IS a source root, do not move. That set is the reason #168 refused the naive fix and it must be re-measured, not assumed.
  • Full corpus bind census, read at source, adjudicated by class.

#168 (recorded this under lib/, fixed the anchor half, left this open), #175 (whose headline repro this blocks), #134 (the "manifests do not guess" precedent), #246 (the other live resolver residual).

Filed 2026-09-10 against master a54cc8f.

Found by the #168/#175 lane, and it is why **#175's headline repro still does not resolve after #175 was fixed**. Two repositories in the pinned corpus hit it, by two different names. ## The mechanism `index::package_root_of` returns the prefix before the first *conventional source directory*: ```rust const SRC_DIRS: &[&str] = &["src", "lib", "tests", "test", "app", "spec", "specs", "source"]; ``` When one of those names is the **first** component, the loop matches at index 0 and returns `""`. `pkg_tail` is then empty, `temp.pkg_lang` skips it (`WHERE fp.pkg_tail != ''`), and **the package can anchor no qualifier at all**. ## Two measured instances, two repositories **`python-flask`.** Every file under `src/flask/` has an empty package root, so `flask` anchors on nothing. This is why `flask.url_for` — the worked example in #175's own body — **still fails after #175's fix**. #175 correctly made the plugin emit a module-qualified call as a *qualified call* rather than a `method_call`; the class is fixed, and this repro needs this defect closed too. **`rust-analyzer`.** #168 recorded the same thing under `lib/`: four real packages live at `lib/line-index`, `lib/la-arena`, `lib/lsp-server`, and `lib/line-index/src/lib.rs` matches `lib` at index 0. So it is not a Python quirk or a Rust quirk — it is the rule, and any repository that puts packages under a top-level directory whose name happens to be in that list loses qualifier anchoring for all of them. ## Why the two directions #168 named were refused, and what changed #168 proposed dropping `lib` from `SRC_DIRS`, or preferring the manifest. Both were measured and refused there. But #168 has since been **fixed by a different shape** — `qual_root_scope_sql`, which gates a stem-anchored candidate on the qualifier's root — and that fix does not touch `package_root_of`. So this half is now cleanly separable in a way it was not when #168 was filed: - `package_root_of` is about **which package a file belongs to**. - `qual_root_scope_sql` is about **which package a qualifier may reach into**. They were entangled while both were broken. Only the first is left. ## Shape of a fix, from #168's own direction 2 `manifest_package_dirs` / `nearest_package_dir` already know that `lib/line-index` holds a `Cargo.toml` and that `src/flask` sits under a `pyproject.toml`. `pkg_tail` is still derived from the **lexical** `package_root_of`. **Deriving the tail from `pkg_dir` when a manifest exists** fixes this class without touching the lexical fallback where no manifest does — the same "manifests do not guess" argument that settled #134. The narrower alternative, also from #168: accept a `SRC_DIRS` name only when it is **not the first component**. `lib/` as a *package container* and `mypkg/lib/` as a *source directory* are different things, and only the second is what the list is for. ## What must NOT be done - **Do not just drop `lib` (or `src`) from the list.** Every Ruby and PHP layout that uses `lib/` as a genuine source root depends on it — measurable on `ruby-sinatra` and `php-guzzle`, and that is exactly why #168 refused this. - **Do not fix it by widening the anchor.** #168 measured what happens: containment alone withdrew **35 correct binds** on rust-analyzer. The anchor is not the broken part. - **Do not measure success by the resolved count.** #175's own close blessed a `−16` on python-flask that was pure phantom removal. Adjudicate binds at source, joined on `(path, line, col, kind, occurrence)` against the prior binary. ## What a fix must prove - `flask.url_for` in `python-flask` resolves to `src/flask/helpers.py:200` — **not** to `src/flask/app.py:1102`, which is the `Flask` method and was the phantom #175 removed. A repro that resolves to the wrong one is worse than one that resolves to nothing. - `lib/line-index` in `rust-analyzer` gets a non-empty package tail. - **Anti-vacuity, and this is the arm that decides it:** `ruby-sinatra` and `php-guzzle`, whose `lib/` genuinely IS a source root, do not move. That set is the reason #168 refused the naive fix and it must be re-measured, not assumed. - Full corpus bind census, read at source, adjudicated by class. ## Related #168 (recorded this under `lib/`, fixed the anchor half, left this open), #175 (whose headline repro this blocks), #134 (the "manifests do not guess" precedent), #246 (the other live resolver residual). Filed 2026-09-10 against `master` `a54cc8f`.
Author
Member

Both directions in "Shape of a fix" fail this issue's own acceptance, in opposite places. Measured before anyone builds either, so the discovery does not happen at the acceptance gate.

The manifest route does not fix the headline repro

The proposal is to derive pkg_tail from pkg_dir when a manifest exists. Where the manifests actually are, in the pinned corpus:

python-flask   pyproject.toml   AT THE REPO ROOT
rust-analyzer  Cargo.toml       at lib/line-index/, crates/*/ …
ruby-sinatra   sinatra.gemspec  AT THE REPO ROOT
php-guzzle     composer.json    AT THE REPO ROOT

So for python-flask the nearest manifest dir is "", the derived tail is empty, and flask still anchors nothing — which is the repro this issue is named for. It fixes rust-analyzer (whose packages really are manifest-bearing subdirectories) and correctly leaves ruby-sinatra and php-guzzle alone, but misses its own first acceptance bullet.

The narrower alternative fixes both anchors and breaks both controls

"Accept a SRC_DIRS name only when it is not the first component", simulated against the exact function body at index.rs:2492:

repo path current tail skip-first tail requirement
python-flask src/flask/helpers.py (empty) flask must anchor — ✓
rust-analyzer lib/line-index/src/lib.rs (empty) line-index must anchor — ✓
ruby-sinatra lib/sinatra/base.rb (empty) sinatra must not move — ✗ MOVES
php-guzzle src/Client.php (empty) src must not move — ✗ MOVES

The php-guzzle row is the clearest tell that this direction is wrong on its own terms: it yields a package tail of src, a source-directory name promoted to a package identity.

What actually distinguishes the four

The same directory name means two different things, and neither the name nor the presence of a manifest somewhere separates them. What separates them is what the directory contains:

lib/ or src/ is… evidence in the tree
rust-analyzer a container of packages its children hold Cargo.toml
python-flask a container of an importable package its child holds __init__.py
ruby-sinatra a source root of one package no child holds a gemspec
php-guzzle a source root (namespace root) children are source files, not package dirs

So the candidate rule is one clause: a SRC_DIRS name at index 0 is skipped only when its immediate children are themselves package units — manifest-bearing, or __init__.py-bearing for Python. Otherwise it is a genuine source root and today's behaviour is already right.

That covers all four rows above and rests on a structural fact rather than a per-language list. It is a DIRECTION, not a result: it needs the full corpus bind census this issue already specifies, adjudicated at source, with ruby-sinatra and php-guzzle proven byte-identical rather than assumed.

One correction to the issue body

flask.url_for … still fails after #175's fix

Worth stating precisely, because I censused it today (3aea49d): after #175, flask.url_for in tests/test_blueprints.py does not resolve at all — it previously resolved to src/flask/app.py:1102, the Flask.url_for METHOD, and that was one of the 16 phantoms #175 removed (verified at source, all 16). So the current state is "correctly unresolved", not "still wrong", and this issue's acceptance bullet already says the right thing: it must reach src/flask/helpers.py:200, and a repro that resolves to app.py:1102 would be worse than one that resolves to nothing.

**Both directions in "Shape of a fix" fail this issue's own acceptance, in opposite places.** Measured before anyone builds either, so the discovery does not happen at the acceptance gate. ## The manifest route does not fix the headline repro The proposal is to derive `pkg_tail` from `pkg_dir` when a manifest exists. Where the manifests actually are, in the pinned corpus: ``` python-flask pyproject.toml AT THE REPO ROOT rust-analyzer Cargo.toml at lib/line-index/, crates/*/ … ruby-sinatra sinatra.gemspec AT THE REPO ROOT php-guzzle composer.json AT THE REPO ROOT ``` So for `python-flask` the nearest manifest dir is `""`, the derived tail is empty, and **`flask` still anchors nothing** — which is the repro this issue is named for. It fixes `rust-analyzer` (whose packages really are manifest-bearing subdirectories) and correctly leaves `ruby-sinatra` and `php-guzzle` alone, but misses its own first acceptance bullet. ## The narrower alternative fixes both anchors and breaks both controls "Accept a `SRC_DIRS` name only when it is not the first component", simulated against the exact function body at `index.rs:2492`: | repo | path | current tail | skip-first tail | requirement | |---|---|---|---|---| | python-flask | `src/flask/helpers.py` | *(empty)* | `flask` | must anchor — **✓** | | rust-analyzer | `lib/line-index/src/lib.rs` | *(empty)* | `line-index` | must anchor — **✓** | | ruby-sinatra | `lib/sinatra/base.rb` | *(empty)* | `sinatra` | must not move — **✗ MOVES** | | php-guzzle | `src/Client.php` | *(empty)* | `src` | must not move — **✗ MOVES** | The `php-guzzle` row is the clearest tell that this direction is wrong on its own terms: it yields a package tail of **`src`**, a source-directory name promoted to a package identity. ## What actually distinguishes the four The same directory name means two different things, and neither the name nor the presence of a manifest *somewhere* separates them. What separates them is **what the directory contains**: | | `lib/` or `src/` is… | evidence in the tree | |---|---|---| | rust-analyzer | a **container of packages** | its children hold `Cargo.toml` | | python-flask | a container of an **importable package** | its child holds `__init__.py` | | ruby-sinatra | a **source root** of one package | no child holds a gemspec | | php-guzzle | a **source root** (namespace root) | children are source files, not package dirs | So the candidate rule is one clause: *a `SRC_DIRS` name at index 0 is skipped only when its immediate children are themselves package units* — manifest-bearing, or `__init__.py`-bearing for Python. Otherwise it is a genuine source root and today's behaviour is already right. That covers all four rows above and rests on a structural fact rather than a per-language list. It is a DIRECTION, not a result: it needs the full corpus bind census this issue already specifies, adjudicated at source, with `ruby-sinatra` and `php-guzzle` proven byte-identical rather than assumed. ## One correction to the issue body > `flask.url_for` … **still fails after #175's fix** Worth stating precisely, because I censused it today (`3aea49d`): after #175, `flask.url_for` in `tests/test_blueprints.py` does not resolve *at all* — it previously resolved to `src/flask/app.py:1102`, the `Flask.url_for` METHOD, and that was one of the 16 phantoms #175 removed (verified at source, all 16). So the current state is "correctly unresolved", not "still wrong", and this issue's acceptance bullet already says the right thing: it must reach `src/flask/helpers.py:200`, and a repro that resolves to `app.py:1102` would be worse than one that resolves to nothing.
Author
Member

The headline fix is already on master in cbbdc04aba. The remaining historical census and mutation-test work is now tracked explicitly in #263. Closing this implementation issue does not claim those residuals are complete.

The headline fix is already on master in cbbdc04aba88f89302dc6fedfd2782d010576a5b. The remaining historical census and mutation-test work is now tracked explicitly in #263. Closing this implementation issue does not claim those residuals are complete.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#247
No description provided.