Repository navigation
tech-debt(analytics): endpoint scan still misses 274 real routes (93.1% of missing findings are false) after the registry-module fixes #12953
Description
Activity
Diagnosed the 71 scanned-but-not-real paths. My hypothesis in the issue body was wrong — I guessed one localized cause because the count barely moved (70 → 70 → 71) across three fixes. It is actually several distinct classes; the count held steady because each fix addressed missed routes, not spurious ones.
Breakdown by source, on indexed source
c5939a51-…:1. Router prefix never resolved — path emitted without its namespace. The largest class.
/api-calls <- api/codebase_analytics/endpoints/api_endpoints.py /api-endpoints <- api/codebase_analytics/endpoints/api_endpoints.py /api/click <- api/vnc_manager.py /api/clipboard <- api/vnc_manager.py /api/cache/evict <- api/system.pyNote
/api-callsis not even under/api/— the module prefix resolved to empty and the route path was concatenated directly, so these cannot match anything. Top namespaces among the 71:users8,session7,knowledge5,macro4,organizations4,teams4.2. Test files contribute endpoints. Small but a genuine correctness defect:
endpoints from test files : 3 spurious (not real) : 2 matching a real route : 0from
api/marketplace_422_test.py,api/schemas_analytics_date_range_test.py,api/user_management/test_tenant_context_resolution.py.scan_all_endpoints()already skips__*andarchive, but not*_test.py/test_*/tests/. A route defined in a test is not an endpoint, and it pollutes both directions — it inflates spurious counts and can surface as a phantom "orphaned" finding telling someone to wire or delete something that does not exist.3. Genuinely-mounted-elsewhere routes, e.g.
/api/chat/completionsfromapi/openai_compat.pyand/api/admin/realtime/bundle/mefromapi/voice_bundle_admin.py— these need checking individually against how they are actually mounted before assuming they are wrong.Suggested order
- Exclude test files from the scan — smallest, unambiguous, no judgement needed.
- Fix the empty-prefix class — the
/api-callsshape proves the module prefix resolved to""rather than to a wrong value, so_get_module_prefix()is falling through to its default for these modules. That likely overlaps with the 274 missed routes: a module whose prefix does not resolve emits its routes at the wrong path, which shows up as one spurious entry and one missed entry. Fixing it should move both counts at once. - Triage class 3 case by case.
That overlap is the useful insight: spurious and missed are not independent residues, so the 274 and the 71 are partly the same defect seen from two sides.
Root cause of the empty-prefix class found — and it is much larger than the 71 spurious paths suggested.
_module_prefix_maphas no entry at all for the affected modules:vnc_manager prefix_map=None api_endpoints prefix_map=None openai_compat prefix_map=None system prefix_map='/api/system' <- one that does resolveSo
_get_module_prefix()falls through to its/apidefault, which is why/api/clickand/api-callscome out without their namespace.Why the map is empty for them.
core_routers.pyregisters routers in two steps, and the parser only understands the second half of it:from api.vnc_manager import router as vnc_router # line 104 -- module -> variable ... (vnc_router, "/vnc", ["vnc"], "vnc"), # line 418 -- variable -> prefix
_parse_config_tuple_registrymatches tuples whose first element is a quoted module path (("services.advanced_workflow.routes", "/advanced-workflow", …)). Here the first element is a bare identifier._apply_dynamic_router_patterndoes handle(router_var, "/prefix", …)tuples, but nothing linksvnc_routerback toapi.vnc_manager, so the prefix is never attributed to a module.Scale — this is not a long tail:
core_routers.py: 90 routers registered as `from api.X import router as Y_router` core_routers.py: 0 config-tuple entries with a quoted module pathNinety modules whose prefix never resolves. That is consistent with the residue being large in both directions, and it confirms the overlap I flagged earlier: a module whose prefix resolves to nothing emits each of its routes at the wrong path, producing one spurious entry and one missed entry per route. So a large share of the remaining 274 missed and 69 spurious should be the same 90 modules.
Fix shape: in
_collect_router_prefixes(), parse thefrom api.<module> import router as <var>aliases in each registry file, then when_apply_dynamic_router_patternsees(<var>, "/prefix", …), attribute that prefix to<module>in_module_prefix_map. Both halves already exist — only the link between them is missing.Expected payoff: this should be the single biggest remaining move on both counts, and unlike the earlier package work it carries little risk of inventing endpoints, because it corrects the prefix of routes the scanner already finds rather than adding new ones. Verify by confirming
real routes matchedrises andscanned but not realfalls together.Test-module exclusion (the other class from my previous comment) is split to #12957 and delivered in PR #12958.
PR #12961 merged as
df0df5002— the empty-prefix class is fixed, and my "90 modules" claim above was wrong; correcting it for the record.The parser does derive a module from the alias by stripping
_router, and that guess is right for 88 of the 90 entries. The defect is narrower: where the alias does not mirror its module, the guess is wrong twice —from api.vnc_manager import router as vnc_router (vnc_router, "/vnc", ["vnc"], "vnc"),
/vncwent toapi.vnc(a different module that also exists, so the error is invisible) whileapi.vnc_managergot nothing and emitted/api/click,/api/clipboard.Two modules affected —
vnc_manager,overseer_handlers— carrying 31 routes:before after real routes matched 1885 1916 (+31) scanned but not real 69 38 (−31) real routes missed 274 243 (−31) missing findings 330 290 false positives 270 239 orphaned findings 290 258 The +31 / −31 / −31 symmetry confirms the overlap predicted earlier: spurious and missed were the same defect from two sides. This is also the first change in the series to move the spurious count, which had sat at 70/70/71/69 throughout.
A detour worth recording: I first applied the fix to
_apply_dynamic_router_patternand nothing moved.core_routers.pyis parsed by_parse_router_registry(simple-tuple format), so the dynamic path never sees the file where all 90 aliases live. Both now carry the fix; only one mattered.Remaining on this issue
243 real routes missed, 38 spurious. Different cause: modules registered in no tuple at all —
api_endpointsandopenai_compatboth resolve toNonefor this reason, not via a bad alias. Next step is to establish how those modules reach the app (nestedinclude_routerchains, or mounts outside the registries entirely) before writing anything.Traced the two
None-prefix modules from my earlier comment. They have different causes, and the second means part of the remaining residue is real signal rather than scanner error.1.
api_endpoints— nestedinclude_routervia relative imports. It is mounted one level below the registry:# api/codebase_analytics/router.py from .endpoints import api_endpoints # 6 of these router.include_router(api_endpoints.router) # 17 include_router calls total
_scan_include_router_patterns()handles include_router chains, but not thisfrom .endpoints import mod+include_router(mod.router)shape. Scope is contained:codebase_analytics/router.pyis the onlyapi/*/router.pyusing it, with 17 sub-routers behind it. That makes it a bounded, well-defined next fix.2.
openai_compat— appears to have no mount site at all. Searching the whole backend finds noinclude_routerfor it and no registry entry; the only references outside its own module are inconftest.py. If that holds,/api/chat/completionsis not served, and the scanner reporting it as "scanned but not inapp.openapi()" is correct — a router that exists and is never mounted.That distinction matters for how this issue should be closed. The remaining 38 spurious are not all scanner defects:
- some are prefix-resolution gaps (fixable, e.g. the nested chains above);
- some are genuine unmounted routers — real findings the audit's own "UNMOUNTED ROUTER MODULES" section exists to report.
Driving the spurious count to zero would therefore be the wrong target: it would mean suppressing true findings. The right target is to fix the resolution gaps and then confirm each survivor is a genuinely dead router, at which point it belongs in the unmounted-router report rather than counted as scan error.
Same caution applies to the 243 missed: some will be dynamically-registered routes no static scan can see. Worth establishing that floor before treating the remainder as a defect.
Suggested next step: extend
_scan_include_router_patterns()to follow relativefrom .<pkg> import <mod>+include_router(<mod>.router), verify against the 17 sub-routers behindcodebase_analytics/router.py, then re-measure and triage what is left.Measured the nested-import fix before writing it, and it is not worth doing as scoped. Correcting my previous comment.
I implied 17 sub-routers were behind the unresolved
codebase_analytics/router.pychain. In practice the existing subdirectory special-case (#1469) already resolves nearly all of them:codebase_analytics endpoints scanned : 84 resolving correctly : 82 wrong : 2 -> /api-calls, /api-endpointsSo following relative
from .endpoints import modchains would recover 2 paths, not a meaningful share of the 38 spurious. Not worth a PR against the rest of the backlog.The larger number nearby is unrelated to prefixes:
real codebase/analytics routes in app.openapi() : 228 missed by the scanner : 50Those 50 are missed routes, not misprefixed ones — so they belong to whatever mounts them, and diagnosing that is the useful next step rather than the import-chain work.
Pattern worth flagging on this issue: three scope estimates on this thread have now come in high — "419 routes behind 7 modules" (actually ~53), "90 modules with unresolved prefixes" (actually 2), and "17 sub-routers" (actually 2). Each time the cheap measurement contradicted the reasoning-from-code-shape. Anyone continuing here should measure the delta on the live indexed source before implementing; it costs one command and has repeatedly changed the decision.
Given that, the highest-value remaining work on this issue is triage, not code: establish how many of the 243 missed are dynamically registered (a floor no static scan can cross) and how many of the 38 spurious are genuinely unmounted routers (true findings). Only what survives both is a defect worth fixing.
- addedarea: code-intelligenceWave 5 · cluster I — Code intelligence & analyticsWave 5 · cluster I — Code intelligence & analytics
on Sep 1, 2026
Follow-on from #12853 → #12944 → #12946 → #12945. Those closed the known scan gaps; this tracks the measured residue, with a concrete baseline instead of a guess.
State after the three fixes
Measured read-only against indexed source
c5939a51-…on the local test install, comparing the scan toapp.openapi()(2159 unique normalized paths):The genuine count holding at exactly 20 across all three fixes is the signal worth keeping: real drift is stable, and every finding removed so far was scan gap, not drift.
What is left
274 real routes still unmatched, and 270 of the 290 unique missing findings are routes that genuinely exist. This is no longer about registry-mounted modules — all 7 are now scanned (6 module-form in #12947, the
llc.apipackage in #12950).Remaining candidates, in rough order of expected yield:
include_routerchains._scan_include_router_patterns()handlesapi/subdirectories, and fix(analytics): resolve registry-mounted package routers via their own APIRouter prefix (#12945) #12950 handles one package level. A router included two levels deep still resolves to the wrong prefix or none.app.include_router(...)inapp_factoryor a lifespan hook, which no registry file names.71 scanned-but-not-real paths also remain — prefix reconstruction is still imperfect somewhere; that number barely moved across all three fixes (70 → 70 → 71), suggesting a single localized cause rather than a spread.
Reproduce
with
norm()collapsing{...}to{p}and stripping a trailing/.Done when
Note
Do not close this by baselining. #12894 showed what suppression does to this exact family of findings — it hid 29 entries behind a label that read as drift and was not.