Skip to content

fix(analytics): resolve registry-mounted package routers via their own APIRouter prefix (#12945) - #12950

Merged
mrveiss merged 3 commits into
Dev_new_guifrom
issue-12945b
Jul 29, 2026
Merged

mrveiss merged 3 commits into
Dev_new_guifrom
issue-12945b

Conversation

@mrveiss

@mrveiss mrveiss commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Closes #12945.

Thinking Path

#12947 handled registry entries naming a module file and deliberately skipped those naming a package, because my first attempt at packages made things measurably worse: applying the registry prefix to each submodule produced 182 endpoints of which 4 were real, pushing scanned-but-not-real from 70 to 209.

Reading how LLC is actually mounted explains why, and what the correct rule is:

feature_routers.py:679   ("llc.api", "", ["llc"], "llc"),     <- empty prefix

llc/api/__init__.py:42   router = APIRouter(prefix="/llc", tags=["llc"])
                  :43+   router.include_router(activity_router)   # no extra prefix
llc/api/costs.py:46      router = APIRouter(prefix="/costs")

So a submodule serves /api + /llc (package router) + /costs (submodule router) + the route path. There is a third prefix level the module-file case does not have, and it lives in the package's own __init__.py — nowhere near the registry entry that names it. Confirmed the sub-routers are included with no additional prefix, so the package router is the only intermediate level.

_scan_file already applies a file's own APIRouter(prefix=…), so only the package-level part belongs in the prefix map.

Two guards against the failure mode that made the first attempt worse:

  • the package must actually mount routers (include_router present in __init__.py), otherwise the entry is not a router package;
  • a submodule is included only if it declares an APIRouter, so helper modules contribute nothing rather than endpoints at a guessed path.

What Changed

api/codebase_analytics/api_endpoint_scanner.py:

  • _registry_router_files() now dispatches a package entry to _package_router_files().
  • _package_router_files() reads the package's __init__.py, takes its router prefix, and maps each router-declaring submodule to registry_prefix + package_prefix.

api_endpoint_scanner_test.py: the placeholder "packages are skipped" test is replaced with four real ones — the prefix coming from the package's own router, the full served path end-to-end (/api/llc/costs/by-agent), helper modules without a router being skipped, and a package that mounts nothing being skipped.

Verification

$ python3 -m pytest api/codebase_analytics/api_endpoint_scanner_test.py -q
19 passed in 1.75s

$ python3 -m pytest api/codebase_analytics/ -q
233 passed, 11 skipped in 5.92s        (was 230 passed / 11 skipped)

$ python3 -m flake8 …/api_endpoint_scanner.py …/api_endpoint_scanner_test.py
(clean)

Measured on the live install's indexed source c5939a51-…, read-only, against app.openapi() (2159 unique normalized paths):

#12947 this PR
router files scanned outside api/ 6 28
endpoints found 2041 2160
real routes matched 1789 (82.9%) 1885 (87.3%)
real routes missed 370 274
scanned but not real 70 71

+96 real routes for +1 spurious. 119 LLC endpoints discovered, 96 of them confirmed present in app.openapi().

End-user metric across the whole thread, on the same source:

before #12943 #12943 #12947 this PR
backend endpoints 0 1986 2041 2160
missing findings ~2400 488 435 330
false positives among them ~97% 95.4% 94.8% 93.1%
genuine findings — 20 20 20

The genuine count holding at exactly 20 across all three fixes is the useful signal: the number of real drift findings is stable, and everything removed so far was scan gap rather than real drift.

What remains (not this issue)

274 real routes are still unmatched and 270 of the 290 unique missing findings remain false positives. That residue is no longer about registry-mounted modules — all 7 are now covered — so it belongs to a different gap (routes reached by neither the api/ walk nor a registry entry, e.g. nested include_router chains). Worth a fresh issue with these numbers as the baseline rather than widening this one.

Model Used

claude-opus-5

…n APIRouter prefix (#12945)

#12947 covered registry entries naming a module file; entries naming a package
were skipped because applying the registry prefix per-submodule invented
endpoints. LLC registers as ('llc.api', '', ...) -- an empty prefix -- while its
real paths come from the package router:

    llc/api/__init__.py:  router = APIRouter(prefix='/llc')
    llc/api/costs.py:     router = APIRouter(prefix='/costs')

so a submodule serves /api + /llc + /costs. The package-level prefix is now read
from its __init__.py and sits between the registry prefix and the submodule
prefix _scan_file already applies.

Submodules are included only when the package actually mounts routers
(include_router present) and the submodule declares one, so helper modules
contribute nothing rather than phantom endpoints.

Measured on indexed source c5939a51 against app.openapi(): external files
6 -> 28, endpoints 2041 -> 2160, real routes matched 1789 (82.9%) -> 1885
(87.3%), routes missed 370 -> 274, scanned-but-not-real 70 -> 71.
@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

@mrveiss

mrveiss commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Reviewed this diff while dogfooding the new secreview skill (#12951 / #12952).
Two confirmed findings, both in _package_router_files — and both are the same
defect class this PR is fixing: a submodule served under a prefix it does not
actually have.

1. rglob descends into nested subpackages, but only the top-level package prefix is applied

sorted(package.rglob("*.py")) walks the whole tree, while
not py_file.name.startswith("__") filters out the nested __init__.py that
carries that subpackage's own prefix. So a nested router package's submodules are
mapped to the parent package's served prefix:

rglob:                          ['__init__.py', 'a.py', 'sub/__init__.py', 'sub/b.py']
filtered (not startswith __):   ['a.py', 'sub/b.py']

sub/b.py is included with the parent's served_prefix; sub/__init__.py — the
only place sub's own APIRouter(prefix=...) could be read — is dropped. If any
registry-mounted package ever nests a router subpackage, this invents endpoints
under the wrong path, exactly the failure mode the PR docstring warns about.

Fix: either glob("*.py") for the flat case, or recurse through
_package_router_files per nested __init__.py so each level contributes its prefix.

2. Docstring overclaims the include_router check

Submodules are included only when the package actually mounts them via include_router

The code checks the package mounts anything:

if not _INCLUDE_ROUTER_RE.search(init_content):
    return {}

then includes every submodule with an APIRouter(prefix=...), mounted or not. A
module that declares a router the package never mounts still contributes routes —
the same phantom-endpoint problem, just narrower. Either match the mounted router
names, or soften the docstring to what the code guarantees.

Not a finding

The package path is built from the registry via joinpath(*module_path.split(".")),
so a .. segment would escape backend_dir — but the registry is code-controlled,
not user input. Noting it only so it is not re-raised later.

Both are pre-existing in this PR's new code rather than regressions of merged
behaviour, so happy for them to land here or as a follow-up — your call.

Applied Black, isort, and autoflake to match code-quality checks.
Triggered by workflow auto-fix-formatting.yml.
@mrveiss
mrveiss merged commit d48146e into Dev_new_gui Jul 29, 2026
39 checks passed
@mrveiss
mrveiss deleted the issue-12945b branch July 29, 2026 07:35
mrveiss added a commit that referenced this pull request Jul 29, 2026
…actually mounted (#12956) (#12970)

* fix(analytics): resolve nested router subpackages and honour what is actually mounted (#12956)

Two defects in _package_router_files, both reported on PR #12950 and both the
class #12945 set out to fix -- a submodule mapped to a prefix it is not served
under, which invents endpoints that resurface as false "orphaned" findings.

1. rglob descended into nested router subpackages while the "__" filter removed
   the very __init__.py carrying their prefix, so their modules were emitted
   under the PARENT's prefix. Now walks one level and recurses, so a nested
   subpackage resolves under its own prefix.

2. The docstring claimed submodules are included "only when the package
   actually mounts them via include_router", but the code only checked that the
   package mounted SOMETHING and then included every router-declaring module.
   Now the alias must be imported (`from .costs import router as costs_router`)
   AND passed to include_router.

Measured on indexed source c5939a51: external files 28 -> 36, endpoints
2157 -> 2220, real routes matched 1916 -> 1960, and scanned-but-not-real
UNCHANGED at 38 -- the tightening gained routes without inventing any.

Three #12950 fixtures mounted an alias without importing it, which no real
registry does (llc/api/__init__.py has 30 such imports). Corrected rather than
loosening the check to accommodate them.

* Auto-fix: code formatting

Applied Black, isort, and autoflake to match code-quality checks.
Triggered by workflow auto-fix-formatting.yml.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant