@gate-forge/pack-fastapi 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -123,6 +123,34 @@ bounded and deterministic:
123
123
  invented). A parameter name shadowing a same-file instance variable
124
124
  keeps the module-level reading only (no double emission).
125
125
 
126
+
127
+ ## Prefixes that are provable, and routers nothing mounts
128
+
129
+ - **Annotated definitions are definitions.** `router: APIRouter =
130
+ APIRouter(prefix="/api/v1")` is indexed exactly like `router =
131
+ APIRouter(prefix="/api/v1")`, prefix included. (It used to be ignored,
132
+ and every route behind it was published at the router's own prefix with
133
+ no typed outcome.)
134
+ - **Constant prefixes fold.** A prefix that is a module-level string
135
+ constant — `API = "/api/v1"` with `APIRouter(prefix=API)` **or**
136
+ `app.include_router(router, prefix=API)` — is provable from the source,
137
+ so the route keeps its real path. An f-string, an attribute, or a call
138
+ is genuinely computed: the router yields a typed
139
+ `FASTAPI_PREFIX_UNRESOLVED` entry and no route is emitted.
140
+ - **A router object built by an unmodeled expression** (`router =
141
+ build_router()`, a subscript, an attribute) has an unknown prefix, so
142
+ its routes are reported as `FASTAPI_PREFIX_UNRESOLVED` naming the
143
+ variable — never as prefix-less paths that no app serves.
144
+ - **`FASTAPI_ROUTER_UNMOUNTED`.** A router that no scanned
145
+ `include_router` targets is served by no scanned app. Its routes keep
146
+ their standalone emission (the declared prefix is all the scan knows),
147
+ and one typed entry names the router, its file and every declared
148
+ route, so "the `/api/v1` prefix was not applied" is never the answer
149
+ when the real one is "nothing mounts this router". Reported only when
150
+ the scanned set shows an application and has no unresolvable include;
151
+ with a partial scan, or an include the scan cannot follow, the pack
152
+ says nothing it cannot prove.
153
+
126
154
  ## Output model (ADR 0004 D1)
127
155
 
128
156
  One `http.contract` resource per (effective mounted path, concrete
@@ -37,7 +37,12 @@ export interface FastapiDetectorOptions {
37
37
  command?: readonly string[];
38
38
  /** Subprocess environment (default: {@link pythonEnvironment}). */
39
39
  env?: NodeJS.ProcessEnv;
40
- /** Working directory the repo-relative paths resolve against. */
40
+ /**
41
+ * Working directory the repo-relative paths resolve against (default:
42
+ * `process.cwd()` AT DISCOVER TIME, not at factory time — the default
43
+ * export is created at module import and `check --staged` moves the
44
+ * process cwd to the candidate checkout before discovery).
45
+ */
41
46
  cwd?: string;
42
47
  /** Handshake-pinned plugin id (default: the pack id). */
43
48
  pluginId?: string;
@@ -49,9 +54,9 @@ export interface FastapiDetectorOptions {
49
54
  */
50
55
  importRoots?: readonly string[];
51
56
  /**
52
- * Repo-relative path of a config document (JSON) read from `cwd` when
53
- * `importRoots` is not given (default: `.gateforge/fastapi.json`;
54
- * absence is normal, malformed throws).
57
+ * Repo-relative path of a config document (JSON) read from the root in
58
+ * force at discover time when `importRoots` is not given (default:
59
+ * `.gateforge/fastapi.json`; absence is normal, malformed throws).
55
60
  */
56
61
  importRootsConfigPath?: string;
57
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AA4CA,2EAA2E;AAC3E,eAAO,MAAM,eAAe,UAAkD,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,4BAA4B,CAAC;AAElE,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAED,eAAO,MAAM,2BAA2B,EAAE,iBAAsB,CAAC;AAsBjE;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,CAkClF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,GAAE,SAAS,MAAM,EAAO,GAAG,MAAM,CAAC,UAAU,CAOlF;AAED,iDAAiD;AACjD,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CACN,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,OAAO,CAAC;QACT,SAAS,EAAE,OAAO,EAAE,CAAC;QACrB,UAAU,EAAE,OAAO,EAAE,CAAC;QACtB,QAAQ,EAAE,OAAO,EAAE,CAAC;QACpB,qBAAqB,EAAE,OAAO,EAAE,CAAC;QACjC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;KACzB,CAAC,CAAC;CACJ;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAuC3F"}
1
+ {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AA6CA,2EAA2E;AAC3E,eAAO,MAAM,eAAe,UAAkD,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,4BAA4B,CAAC;AAElE,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAED,eAAO,MAAM,2BAA2B,EAAE,iBAAsB,CAAC;AAsBjE;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,CAkClF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,GAAE,SAAS,MAAM,EAAO,GAAG,MAAM,CAAC,UAAU,CAOlF;AAED,iDAAiD;AACjD,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CACN,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,OAAO,CAAC;QACT,SAAS,EAAE,OAAO,EAAE,CAAC;QACrB,UAAU,EAAE,OAAO,EAAE,CAAC;QACtB,QAAQ,EAAE,OAAO,EAAE,CAAC;QACpB,qBAAqB,EAAE,OAAO,EAAE,CAAC;QACjC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;KACzB,CAAC,CAAC;CACJ;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAiD3F"}
package/dist/detector.js CHANGED
@@ -22,10 +22,11 @@
22
22
  * handler corroboration over the facts this pack emits).
23
23
  *
24
24
  * Determinism: the python scan is pure over (paths, file bytes). The
25
- * optional `.gateforge/fastapi.json` config (import roots for absolute
26
- * imports, the central-router-registry pattern) is read once at factory
27
- * time and passed to the scanner as an explicit `--import-roots` argv
28
- * flag — never environment state, never per-request mutation.
25
+ * The optional `.gateforge/fastapi.json` config (import roots for absolute
26
+ * imports, the central-router-registry pattern) is read from the root that
27
+ * is in force at DISCOVER time and passed to the scanner as an explicit
28
+ * `--import-roots` argv flag — never environment state, never per-request
29
+ * mutation.
29
30
  */
30
31
  import { readFileSync } from 'node:fs';
31
32
  import { delimiter, resolve } from 'node:path';
@@ -120,24 +121,35 @@ export function pythonEnvironment(extra = []) {
120
121
  export function createFastapiDetector(options = {}) {
121
122
  const command = options.command ?? DEFAULT_COMMAND;
122
123
  const env = options.env ?? pythonEnvironment();
123
- const cwd = options.cwd ?? process.cwd();
124
124
  const pluginId = options.pluginId ?? PACK_PLUGIN_ID;
125
125
  const pluginVersion = options.pluginVersion ?? PACK_VERSION;
126
- // Explicit roots win; otherwise the config document (absence normal,
127
- // malformed throws). With no roots at all the spawned command is
128
- // byte-identical to the pre-config surface.
129
- const importRoots = options.importRoots ??
130
- readFastapiScanConfigOrNull(resolve(cwd, options.importRootsConfigPath ?? FASTAPI_SCAN_CONFIG_PATH))
131
- .importRoots ??
132
- [];
133
- const argv = importRoots.length > 0 ? [...command, '--import-roots', JSON.stringify(importRoots)] : [...command];
126
+ // The repo root is resolved at DISCOVER time unless the caller pinned one
127
+ // explicitly: the default export of this pack is created at module import
128
+ // (the CLI imports it at startup), and `gateforge check --staged` moves the
129
+ // process cwd to the staged candidate checkout before discovery runs. A
130
+ // root captured at factory time would pin the loader's cwd and read the
131
+ // user's worktree bytes instead of the gated ones. The same holds for the
132
+ // config document: `.gateforge/fastapi.json` is read from the root in
133
+ // force at this discover call. Explicit options always win.
134
+ const resolveRoot = () => options.cwd ?? process.cwd();
135
+ const resolveArgv = (cwd) => {
136
+ // Explicit roots win; otherwise the config document (absence normal,
137
+ // malformed throws). With no roots at all the spawned command is
138
+ // byte-identical to the pre-config surface.
139
+ const importRoots = options.importRoots ??
140
+ readFastapiScanConfigOrNull(resolve(cwd, options.importRootsConfigPath ?? FASTAPI_SCAN_CONFIG_PATH))
141
+ .importRoots ??
142
+ [];
143
+ return importRoots.length > 0 ? [...command, '--import-roots', JSON.stringify(importRoots)] : [...command];
144
+ };
134
145
  return {
135
146
  async discover(paths) {
136
147
  if (paths.length === 0) {
137
148
  return { resources: [], unresolved: [], findings: [], classificationSignals: [] };
138
149
  }
150
+ const cwd = resolveRoot();
139
151
  const session = new PluginSession({
140
- command: argv,
152
+ command: resolveArgv(cwd),
141
153
  pluginId,
142
154
  pluginVersion,
143
155
  cwd,
@@ -1 +1 @@
1
- {"version":3,"file":"detector.js","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"detector.js","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gate-forge/pack-fastapi",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "FastAPI server-route detector: Python AST scanner served over GPP/3 (pack-sqlalchemy pattern), emitting http.contract evidence facts with canonical effective paths plus exposure/lifecycle signals (ADR 0004 D1, plan phase 2).",
6
6
  "type": "module",
@@ -19,7 +19,10 @@
19
19
  "files": [
20
20
  "README.md",
21
21
  "dist",
22
- "python"
22
+ "python",
23
+ "!**/__pycache__",
24
+ "!**/*.pyc",
25
+ "!**/*.pyo"
23
26
  ],
24
27
  "scripts": {
25
28
  "build": "tsc -p tsconfig.build.json",
@@ -27,9 +30,9 @@
27
30
  "test": "vitest run"
28
31
  },
29
32
  "dependencies": {
30
- "@gate-forge/core": "^0.8.0",
31
- "@gate-forge/http-contract": "^0.8.0",
32
- "@gate-forge/plugin-protocol": "^0.8.0",
33
+ "@gate-forge/core": "^0.9.0",
34
+ "@gate-forge/http-contract": "^0.9.0",
35
+ "@gate-forge/plugin-protocol": "^0.9.0",
33
36
  "zod": "^4.1.5"
34
37
  },
35
38
  "publishConfig": {
@@ -19,9 +19,26 @@ Detector vocabulary (frozen with the pack):
19
19
  - One fact per (effective mounted path, concrete method): an
20
20
  ``api_route(methods=[...])`` yields one fact per listed method, and a
21
21
  router mounted twice yields one fact per mount (plan phase 2.3).
22
- - Standalone routers (never the target of a resolvable ``include_router``)
23
- emit their routes at their own prefix with ``mountProvenance``
24
- ``standalone`` — matching the legacy wiring scanner's default mount.
22
+ ``Router creation that the AST pass cannot model`` (``build_router()``,
23
+ a subscript, an attribute, any expression that is not a plain
24
+ ``APIRouter(...)`` call) is a typed ``FASTAPI_PREFIX_UNRESOLVED``
25
+ outcome naming the variable and its file — NEVER a silently
26
+ prefix-less router: a route whose effective path cannot be proven is
27
+ never reported as served.
28
+ - Literal prefixes: a prefix expression that is a module-level string
29
+ constant (``PREFIX = "/api/v1"``) is folded to its literal, for both
30
+ ``APIRouter(prefix=...)`` and ``include_router(prefix=...)``, so the
31
+ mount prefix reaches the effective path. Anything else computed stays
32
+ typed-unresolved.
33
+ - Annotated router definitions (``router: APIRouter = APIRouter(...)``)
34
+ are router definitions exactly like ``router = APIRouter(...)``,
35
+ prefix included.
36
+ - Unmounted routers: when the whole mount graph is provable (no
37
+ ``FASTAPI_PREFIX_UNRESOLVED`` anywhere in the scan), a router that no
38
+ include edge targets is dead code in the scanned set: its routes keep
39
+ their standalone emission, and one ``FASTAPI_ROUTER_UNMOUNTED`` entry
40
+ names the router, its file, and every declared route so the reader can
41
+ see that no scanned app serves those paths.
25
42
  - Registry functions (the ``def register_all_routers(app): ...
26
43
  app.include_router(r, prefix=...)`` pattern): a function whose body
27
44
  calls ``include_router`` on one of ITS OWN parameters collects those
@@ -65,10 +82,14 @@ Detector vocabulary (frozen with the pack):
65
82
  imports are unaffected. Without import roots every behavior is exactly
66
83
  as before (closed-world: back-compat).
67
84
  - ``unresolved`` entries: ``FASTAPI_PREFIX_UNRESOLVED`` for computed
68
- router/include prefixes, unresolvable or ambiguous include
69
- targets/imports/aliases, unresolvable registry-function call-site
70
- arguments, include cycles, and registry chains beyond the helper-depth
71
- bound; ``HTTP_PATH_DYNAMIC`` for non-literal route paths;
85
+ router/include prefixes, router creations the AST pass cannot model,
86
+ unresolvable or ambiguous include targets/imports/aliases,
87
+ unresolvable registry-function call-site arguments, include cycles,
88
+ and registry chains beyond the helper-depth bound;
89
+ ``FASTAPI_ROUTER_UNMOUNTED`` for a router no include edge targets —
90
+ reported only when the scanned set shows an application and no
91
+ unresolvable include, since "unmounted" is otherwise unprovable;
92
+ ``HTTP_PATH_DYNAMIC`` for non-literal route paths;
72
93
  ``HTTP_METHOD_DYNAMIC`` for decorator verbs outside the supported set.
73
94
  All are source-located and blocking — nothing disappears silently.
74
95
  - No app import, no route execution, no environment or network access
@@ -89,6 +110,10 @@ FRAMEWORK = "fastapi"
89
110
 
90
111
  # Typed outcome codes (mirrored in @gate-forge/http-contract codes.ts).
91
112
  FASTAPI_PREFIX_UNRESOLVED = "FASTAPI_PREFIX_UNRESOLVED"
113
+ # A router no include edge targets, reported only when the mount graph
114
+ # itself is provable: the paths the standalone fallback emits are served
115
+ # by no scanned app, and the reader must be told which file declares them.
116
+ FASTAPI_ROUTER_UNMOUNTED = "FASTAPI_ROUTER_UNMOUNTED"
92
117
 
93
118
  _DECORATOR_METHODS = {
94
119
  "get": "GET",
@@ -150,6 +175,13 @@ class RouterDef:
150
175
  # Set when the variable is an import alias bound to a router defined in
151
176
  # another scanned module: ``(raw_module, level, imported_name)``.
152
177
  alias_of: tuple[str | None, int, str] | None = None
178
+ # True when the router OBJECT comes from an expression this AST pass
179
+ # cannot model (``build_router()``, a subscript, an attribute, any
180
+ # expression that is not a plain ``APIRouter(...)`` call). Its own
181
+ # prefix and therefore every route's effective path are unprovable:
182
+ # the walk reports a typed outcome instead of emitting a fabricated
183
+ # prefix-less path.
184
+ creation_computed: bool = False
153
185
 
154
186
 
155
187
  @dataclass
@@ -280,6 +312,41 @@ def _static_string(node: ast.AST | None) -> str | None:
280
312
  return None
281
313
 
282
314
 
315
+ def _literal_prefix(node: ast.AST | None, constants: dict[str, str]) -> str | None:
316
+ """A mount prefix that provably is one literal string, else None.
317
+
318
+ Beyond a literal (``prefix="/api/v1"``) this folds a module-level
319
+ string CONSTANT (``PREFIX = "/api/v1"; APIRouter(prefix=PREFIX)``):
320
+ that value is fixed by the source, so the effective path is provable
321
+ and the route must not disappear behind a computed-prefix outcome.
322
+ Anything else (an f-string, an attribute, a call, a concatenation)
323
+ stays computed and is reported as typed-unresolved.
324
+ """
325
+ literal = _static_string(node)
326
+ if literal is not None:
327
+ return literal
328
+ if isinstance(node, ast.Name):
329
+ return constants.get(node.id)
330
+ return None
331
+
332
+
333
+ def _module_string_constants(tree: ast.Module) -> dict[str, str]:
334
+ """Module-level string constants, for literal prefix folding."""
335
+ constants: dict[str, str] = {}
336
+ for statement in tree.body:
337
+ if isinstance(statement, ast.Assign):
338
+ literal = _static_string(statement.value)
339
+ if literal is not None:
340
+ for target in statement.targets:
341
+ if isinstance(target, ast.Name):
342
+ constants[target.id] = literal
343
+ elif isinstance(statement, ast.AnnAssign):
344
+ literal = _static_string(statement.value)
345
+ if literal is not None and isinstance(statement.target, ast.Name):
346
+ constants[statement.target.id] = literal
347
+ return constants
348
+
349
+
283
350
  def _dotted_name(node: ast.AST | None) -> str | None:
284
351
  """A dotted name for Name/Attribute chains, or None."""
285
352
  if isinstance(node, ast.Name):
@@ -402,13 +469,14 @@ class _ModuleVisitor(ast.NodeVisitor):
402
469
  its ``include_router`` calls still count for the outer parameter.
403
470
  """
404
471
 
405
- def __init__(self, relpath: str) -> None:
472
+ def __init__(self, relpath: str, constants: dict[str, str] | None = None) -> None:
406
473
  self.index = FileIndex(relpath=relpath)
407
474
  # Current top-level function (registry-function context), or None
408
475
  # at module level.
409
476
  self._function: FunctionIncludes | None = None
410
-
411
- # -- imports ------------------------------------------------------------
477
+ # Module-level string constants, for literal prefix folding
478
+ # (`PREFIX = "/api/v1"`; `APIRouter(prefix=PREFIX)`).
479
+ self._constants: dict[str, str] = constants or {}
412
480
 
413
481
  def visit_Import(self, node: ast.Import) -> None:
414
482
  for alias in node.names:
@@ -425,25 +493,43 @@ class _ModuleVisitor(ast.NodeVisitor):
425
493
  # -- router / app instances ---------------------------------------------
426
494
 
427
495
  def visit_Assign(self, node: ast.Assign) -> None:
428
- if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name):
429
- kind = node.value.func.id
430
- if kind in {"FastAPI", "APIRouter"}:
431
- for target in node.targets:
432
- if not isinstance(target, ast.Name):
433
- continue
434
- if kind == "FastAPI":
435
- self.index.apps.add(target.id)
436
- else:
437
- prefix_node = _keyword(node.value, "prefix")
438
- prefix: str | None = (
439
- "" if prefix_node is None else _static_string(prefix_node)
440
- )
441
- self.index.routers[target.id] = RouterDef(
442
- var=target.id, prefix=prefix,
443
- prefix_node=prefix_node if prefix_node is not None else node.value,
444
- )
496
+ for target in node.targets:
497
+ if isinstance(target, ast.Name):
498
+ self._record_instance(target.id, node.value)
445
499
  self.generic_visit(node)
446
500
 
501
+ def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
502
+ # `router: APIRouter = APIRouter(prefix="/api/v1")` defines a
503
+ # router exactly like the unannotated form; before this, the
504
+ # annotated shape produced a silently prefix-less route.
505
+ if node.value is not None and isinstance(node.target, ast.Name):
506
+ self._record_instance(node.target.id, node.value)
507
+ self.generic_visit(node)
508
+
509
+ def _record_instance(self, name: str, value: ast.AST) -> None:
510
+ """Indexes one ``FastAPI()`` / ``APIRouter()`` binding."""
511
+ if not (isinstance(value, ast.Call) and isinstance(value.func, ast.Name)):
512
+ return
513
+ kind = value.func.id
514
+ if kind == "FastAPI":
515
+ self.index.apps.add(name)
516
+ # Routes declared straight on the app (`@app.get("/health")`)
517
+ # belong to it with no router prefix of their own.
518
+ self.index.routers.setdefault(
519
+ name, RouterDef(var=name, prefix="", prefix_node=value),
520
+ )
521
+ return
522
+ if kind != "APIRouter":
523
+ return
524
+ prefix_node = _keyword(value, "prefix")
525
+ prefix: str | None = (
526
+ "" if prefix_node is None else _literal_prefix(prefix_node, self._constants)
527
+ )
528
+ self.index.routers[name] = RouterDef(
529
+ var=name, prefix=prefix,
530
+ prefix_node=prefix_node if prefix_node is not None else value,
531
+ )
532
+
447
533
  # -- response models -----------------------------------------------------
448
534
 
449
535
  def visit_ClassDef(self, node: ast.ClassDef) -> None:
@@ -561,7 +647,19 @@ class _ModuleVisitor(ast.NodeVisitor):
561
647
  alias_of = None
562
648
  if ref is not None and ref.name is not None:
563
649
  alias_of = (ref.module, ref.level, ref.name)
564
- router = RouterDef(var=owner.id, prefix="", prefix_node=owner, alias_of=alias_of)
650
+ # No router definition and no import binding for this name: the
651
+ # router object comes from an expression this pass cannot model
652
+ # (`build_router()`, a subscript, a re-assignment). Its own
653
+ # prefix is therefore UNKNOWN — a synthetic `prefix=""` here
654
+ # would publish every one of its routes at a path no app serves,
655
+ # with no typed outcome at all.
656
+ router = RouterDef(
657
+ var=owner.id,
658
+ prefix="" if alias_of is not None else None,
659
+ prefix_node=owner,
660
+ alias_of=alias_of,
661
+ creation_computed=alias_of is None,
662
+ )
565
663
  self.index.routers[owner.id] = router
566
664
  router.routes.append(entry)
567
665
  out_of_set = [m for m in methods if m not in _DECORATOR_METHODS.values()]
@@ -600,7 +698,10 @@ class _ModuleVisitor(ast.NodeVisitor):
600
698
  target_var=target_var,
601
699
  target_alias=target_alias,
602
700
  target_attrs=target_attrs,
603
- prefix="" if prefix_node is None else _static_string(prefix_node),
701
+ prefix=(
702
+ "" if prefix_node is None
703
+ else _literal_prefix(prefix_node, self._constants)
704
+ ),
604
705
  node=node,
605
706
  file=self.index.relpath,
606
707
  )
@@ -657,7 +758,7 @@ def _scan_file(relpath: str, root: Path) -> tuple[FileIndex | None, dict | None]
657
758
  "detail": f"{type(exc).__name__}: {msg}",
658
759
  "locations": [{"file": relpath, "line": max(line, 1), "col": 0}],
659
760
  }
660
- visitor = _ModuleVisitor(relpath)
761
+ visitor = _ModuleVisitor(relpath, _module_string_constants(tree))
661
762
  visitor.visit(tree)
662
763
  return visitor.index, None
663
764
 
@@ -893,6 +994,7 @@ class _Resolver:
893
994
  self._materialize_function_includes()
894
995
  self._merge_aliases()
895
996
  included = self._collect_included()
997
+ unmounted: list[tuple[str, str, RouterDef]] = []
896
998
  for relpath in sorted(self.indexes):
897
999
  index = self.indexes[relpath]
898
1000
  for var in sorted(index.apps):
@@ -906,6 +1008,56 @@ class _Resolver:
906
1008
  if (relpath, name) in included:
907
1009
  continue
908
1010
  self._walk(relpath, name, "", (), "standalone", included)
1011
+ if router.routes:
1012
+ unmounted.append((relpath, name, router))
1013
+ self._report_unmounted_routers(
1014
+ unmounted, apps_present=any(index.apps for index in self.indexes.values()),
1015
+ )
1016
+
1017
+ def _report_unmounted_routers(
1018
+ self,
1019
+ unmounted: list[tuple[str, str, RouterDef]],
1020
+ apps_present: bool,
1021
+ ) -> None:
1022
+ """Names routers no include edge mounts, when that is provable.
1023
+
1024
+ A router that no scanned include edge targets is served by no
1025
+ scanned app: the standalone fallback still reports its routes (a
1026
+ route's path is a claim, and its declared prefix is all this pass
1027
+ knows), but the reader must see that nothing mounts them — the
1028
+ routes appear at the router's OWN prefix, without whatever mount
1029
+ prefix the sibling modules carry.
1030
+
1031
+ Reported ONLY when the scanned set actually shows the application
1032
+ and its mount graph is otherwise fully proven. With no app
1033
+ instance the mounting code may simply be outside the scan, and
1034
+ with an unresolvable include this scan cannot say which routers
1035
+ that include would have reached — in both cases "unmounted" would
1036
+ be a claim about files nobody could see, or would bury the real
1037
+ blocker (already reported) under unrelated entries.
1038
+ """
1039
+ if not apps_present:
1040
+ return
1041
+ if any(entry["code"] == FASTAPI_PREFIX_UNRESOLVED for entry in self.unresolved):
1042
+ return
1043
+ for relpath, name, router in unmounted:
1044
+ routes = sorted(
1045
+ f"{method} {router.prefix or ''}{route.path}"
1046
+ for route in router.routes
1047
+ if route.path is not None
1048
+ for method in route.methods
1049
+ )
1050
+ self.unresolved.append({
1051
+ "code": FASTAPI_ROUTER_UNMOUNTED,
1052
+ "detail": (
1053
+ f"router '{name}' in {relpath} is never included by any app in the "
1054
+ f"scanned set; its {len(routes)} route(s) "
1055
+ f"({', '.join(routes)}) are reported at the router's own prefix, "
1056
+ "which no scanned app serves — mount the router, or delete it if it "
1057
+ "is dead code"
1058
+ ),
1059
+ "location": loc(relpath, router.prefix_node),
1060
+ })
909
1061
 
910
1062
  def _materialize_function_includes(self) -> None:
911
1063
  """Rewires registry-function includes onto real instances.
@@ -1381,10 +1533,17 @@ class _Resolver:
1381
1533
  return
1382
1534
  if router.prefix is None:
1383
1535
  self.unresolved.append({
1384
- "code": "FASTAPI_PREFIX_UNRESOLVED",
1536
+ "code": FASTAPI_PREFIX_UNRESOLVED,
1385
1537
  "detail": (
1386
- f"router '{var}' in {relpath} declares a computed prefix; "
1387
- "the effective path cannot be proven statically"
1538
+ (
1539
+ f"router '{var}' in {relpath} is created by an expression that "
1540
+ "cannot be modeled statically (its own prefix and routes are "
1541
+ "unknown), so its effective paths are not reported"
1542
+ )
1543
+ if router.creation_computed else (
1544
+ f"router '{var}' in {relpath} declares a computed prefix; "
1545
+ "the effective path cannot be proven statically"
1546
+ )
1388
1547
  ),
1389
1548
  "location": loc(relpath, router.prefix_node),
1390
1549
  })