@gate-forge/pack-fastapi 0.7.0 → 0.8.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
@@ -145,6 +145,45 @@ handler names these very facts carry, and blocks ambiguity with typed
145
145
  detection remains for genuinely stale authority signals (declaration
146
146
  markers, adapter bindings, read-only declarations).
147
147
 
148
+ ## Response-model wire names
149
+
150
+ Each server-route fact carries `attributes.responseModelFields`: the wire
151
+ names its response model answers to, in declaration order — every pydantic
152
+ field name plus every `Field(alias=...)` it declares, including the fields
153
+ it inherits from a base class the scanned set also proves. The model is
154
+ the decorator's `response_model=` when it declares one and the handler's
155
+ return annotation otherwise (FastAPI's own default), with the containers
156
+ FastAPI unwraps peeled off: `list[InvoiceOut]`, `Optional[MoneyOut]` and
157
+ `MoneyOut | None` all report the element model.
158
+
159
+ `detail` is always among the names: FastAPI answers a failed request with
160
+ `{"detail": ...}` (an `HTTPException`) or a `detail` list (request
161
+ validation), so a frontend reading `detail` to report an error is never
162
+ evidence of a dropped success-model field.
163
+
164
+ The attribute is **absent** whenever the wire names are not statically
165
+ computable, never partial:
166
+
167
+ - a model configuring alias generation (`model_config = ConfigDict(...)`
168
+ or a pydantic v1 `class Config`) — every name differs;
169
+ - a base class outside the scanned set — inherited fields unknown;
170
+ - a shape that is not one model: `dict`, a union of two models, a
171
+ computed annotation, no annotation at all.
172
+
173
+ The existing `responseModel` attribute keeps its exact previous meaning
174
+ (the decorator declaration only) and its exact previous bytes; a return
175
+ annotation is a new fact, never a change of the old one.
176
+
177
+ `gateforge check` cross-checks these names against the fields a frontend
178
+ actually reads (reported by `@gate-forge/pack-http` as `responseReads`)
179
+ and emits one **non-blocking** `RESPONSE_FIELD_MISSING_FROM_MODEL`
180
+ advisory per field a joined endpoint's model does not declare — naming the
181
+ endpoint, the field, the frontend file and line, and the names the model
182
+ does declare. A field counts as present under its own name, under the other
183
+ case style (`due_date` ≡ `dueDate`) or under a declared alias. A
184
+ repository whose routes declare no provable response model produces no
185
+ advisory and a byte-identical report.
186
+
148
187
  ## Setup
149
188
 
150
189
  ```yaml
@@ -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,CAGlF;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":"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"}
package/dist/detector.js CHANGED
@@ -107,7 +107,11 @@ export function readFastapiScanConfigOrNull(path) {
107
107
  */
108
108
  export function pythonEnvironment(extra = []) {
109
109
  const entries = [...extra, PACK_PYTHON_DIR, PROTOCOL_PYTHON_DIR];
110
- return { ...process.env, PYTHONPATH: entries.join(delimiter) };
110
+ return {
111
+ ...process.env,
112
+ PYTHONDONTWRITEBYTECODE: '1',
113
+ PYTHONPATH: entries.join(delimiter),
114
+ };
111
115
  }
112
116
  /**
113
117
  * Creates a discover-capable detector module. The default export of the
@@ -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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"facts.d.ts","sourceRoot":"","sources":["../src/facts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,qEAAqE;AACrE,KAAK,QAAQ,GAAG,YAAY,CAAC;AAE7B,4EAA4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,CAAC,CAAC;IACjB,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,uBAAuB;IACtC,4EAA4E;IAC5E,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,gEAAgE;IAChE,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;IACxE,kEAAkE;IAClE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;CAC/C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,SAAS,gBAAgB,EAAE,GACrC,uBAAuB,CA0EzB"}
1
+ {"version":3,"file":"facts.d.ts","sourceRoot":"","sources":["../src/facts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,qEAAqE;AACrE,KAAK,QAAQ,GAAG,YAAY,CAAC;AAE7B,4EAA4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,CAAC,CAAC;IACjB,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,uBAAuB;IACtC,4EAA4E;IAC5E,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,gEAAgE;IAChE,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;IACxE,kEAAkE;IAClE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;CAC/C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,SAAS,gBAAgB,EAAE,GACrC,uBAAuB,CAqFzB"}
package/dist/facts.js CHANGED
@@ -81,6 +81,16 @@ export function canonicalizeFacts(resources) {
81
81
  if (typeof responseModel === 'string' && responseModel.length > 0) {
82
82
  fact.responseSchemaSymbols = [responseModel];
83
83
  }
84
+ // Wire names the response model answers to (plan 2026-09-25 Phase 4b
85
+ // item 5). The python scanner emits them only for a provably
86
+ // concrete model, so anything else arrives absent here too and the
87
+ // shared fact carries no field list to compare frontend reads against.
88
+ const responseModelFields = attributes['responseModelFields'];
89
+ if (Array.isArray(responseModelFields) && responseModelFields.length > 0) {
90
+ const names = responseModelFields.filter((name) => typeof name === 'string' && name.length > 0);
91
+ if (names.length === responseModelFields.length)
92
+ fact.responseModelFields = names;
93
+ }
84
94
  const requestSchemas = attributes['requestSchemaSymbols'];
85
95
  if (Array.isArray(requestSchemas)) {
86
96
  const names = requestSchemas.filter((name) => typeof name === 'string' && name.length > 0);
package/dist/facts.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"facts.js","sourceRoot":"","sources":["../src/facts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"facts.js","sourceRoot":"","sources":["../src/facts.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.7.0",
3
+ "version": "0.8.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",
@@ -27,9 +27,9 @@
27
27
  "test": "vitest run"
28
28
  },
29
29
  "dependencies": {
30
- "@gate-forge/core": "^0.7.0",
31
- "@gate-forge/http-contract": "^0.7.0",
32
- "@gate-forge/plugin-protocol": "^0.7.0",
30
+ "@gate-forge/core": "^0.8.0",
31
+ "@gate-forge/http-contract": "^0.8.0",
32
+ "@gate-forge/plugin-protocol": "^0.8.0",
33
33
  "zod": "^4.1.5"
34
34
  },
35
35
  "publishConfig": {
@@ -128,6 +128,11 @@ class RouteDef:
128
128
  handler: str # function name
129
129
  is_async: bool
130
130
  response_model: str | None
131
+ # The model FastAPI actually answers with: the decorator's explicit
132
+ # `response_model=` when present, else the handler's return annotation
133
+ # (FastAPI's own default). Additive: `response_model` above keeps its
134
+ # exact previous meaning and emission.
135
+ effective_response_model: str | None
131
136
  request_schemas: list[str]
132
137
  tags: list[str]
133
138
  operation_id: str | None
@@ -222,6 +227,32 @@ class ImportRef:
222
227
  level: int # relative-import depth (0: absolute)
223
228
 
224
229
 
230
+ @dataclass
231
+ class ModelDef:
232
+ """One class definition whose response fields are statically provable.
233
+
234
+ Plan 2026-09-25 Phase 4b item 5: a dropped response-model field is
235
+ invisible from the frontend (every test mocks it), so the pack proves
236
+ the wire names a model answers to. ``proven`` is the fail-closed
237
+ switch: a model that configures alias generation (pydantic v1
238
+ ``class Config`` / v2 ``model_config``) has wire names this AST pass
239
+ cannot compute, so it is reported as NOT proven and contributes no
240
+ field list at all — silence, never a wrong answer.
241
+ """
242
+
243
+ name: str
244
+ fields: list[str] = field(default_factory=list) # declared names, written order
245
+ aliases: dict[str, str] = field(default_factory=dict) # field name -> declared alias
246
+ bases: list[str] = field(default_factory=list) # base class names
247
+ proven: bool = True
248
+
249
+
250
+ # Base classes that carry no response fields of their own. Any OTHER base
251
+ # must resolve inside the scanned set: an unresolvable base means inherited
252
+ # fields are unknown, so the model is not proven.
253
+ _FIELDLESS_BASES = frozenset({"BaseModel", "BaseModelRoot", "RootModel", "object"})
254
+
255
+
225
256
  @dataclass
226
257
  class FileIndex:
227
258
  """Everything one parsed file contributes to the mount graph."""
@@ -234,6 +265,7 @@ class FileIndex:
234
265
  functions: dict[str, FunctionIncludes] = field(default_factory=dict)
235
266
  helper_calls: list[HelperCall] = field(default_factory=list)
236
267
  unsupported: list[tuple[ast.AST, list[str]]] = field(default_factory=list)
268
+ models: dict[str, ModelDef] = field(default_factory=dict)
237
269
 
238
270
 
239
271
  def loc(relpath: str, node: ast.AST) -> dict:
@@ -258,6 +290,42 @@ def _dotted_name(node: ast.AST | None) -> str | None:
258
290
  return None
259
291
 
260
292
 
293
+ # Containers whose single element type IS the response model
294
+ # (``-> list[InvoiceOut]``); anything else (``dict[str, X]``, a union of
295
+ # two models, a computed expression) is an unknown shape.
296
+ _MODEL_CONTAINERS = frozenset({
297
+ "list", "List", "Sequence", "Iterable", "set", "Set", "frozenset",
298
+ "Optional", "Union", "tuple", "Tuple",
299
+ })
300
+
301
+
302
+ def _model_name(node: ast.AST | None) -> str | None:
303
+ """The model class a response annotation or ``response_model=`` names.
304
+
305
+ Unwraps the containers FastAPI itself unwraps (``list[X]``,
306
+ ``Optional[X]``, ``X | None``) and returns the dotted class name. Any
307
+ other shape — a dict, a union of different models, a computed
308
+ expression — returns None: an unknown shape has no field list, and the
309
+ advisory must never guess one.
310
+ """
311
+ if node is None:
312
+ return None
313
+ if isinstance(node, (ast.Name, ast.Attribute)):
314
+ return _dotted_name(node)
315
+ if isinstance(node, ast.Subscript):
316
+ base = _dotted_name(node.value)
317
+ if base is None or base.split(".")[-1] not in _MODEL_CONTAINERS:
318
+ return None
319
+ elements = _elements(node.slice) or [node.slice]
320
+ names = {_model_name(element) for element in elements}
321
+ names.discard(None)
322
+ return names.pop() if len(names) == 1 else None
323
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
324
+ left, right = _model_name(node.left), _model_name(node.right)
325
+ return left if left is not None and left == right else None
326
+ return None
327
+
328
+
261
329
  def _keyword(call: ast.Call, name: str) -> ast.AST | None:
262
330
  for keyword in call.keywords:
263
331
  if keyword.arg == name:
@@ -271,6 +339,61 @@ def _elements(node: ast.AST | None) -> list[ast.AST] | None:
271
339
  return None
272
340
 
273
341
 
342
+ def _declared_alias(value: ast.AST | None) -> str | None:
343
+ """The static alias a ``Field(alias=...)`` declaration carries."""
344
+ if not isinstance(value, ast.Call):
345
+ return None
346
+ for keyword in value.keywords:
347
+ if keyword.arg in {"alias", "validation_alias", "serialization_alias"}:
348
+ return _static_string(keyword.value)
349
+ return None
350
+
351
+
352
+ def _is_class_var(annotation: ast.AST | None) -> bool:
353
+ """``ClassVar[...]`` annotations are not response fields."""
354
+ if not isinstance(annotation, ast.Subscript):
355
+ return False
356
+ base = _dotted_name(annotation.value)
357
+ return base is not None and base.split(".")[-1] == "ClassVar"
358
+
359
+
360
+ def _model_def(node: ast.ClassDef) -> ModelDef:
361
+ """The statically provable field list of one class definition.
362
+
363
+ Alias generation (``model_config = ConfigDict(alias_generator=...)``
364
+ or a pydantic v1 ``class Config``) makes the wire names
365
+ uncomputable here, so the model is reported as NOT proven rather than
366
+ as a partial list. ``ClassVar`` members and private names are not
367
+ pydantic fields and are skipped.
368
+ """
369
+ model = ModelDef(name=node.name)
370
+ for base in node.bases:
371
+ name = _dotted_name(base)
372
+ model.bases.append(name if name is not None else "?")
373
+ for statement in node.body:
374
+ if isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name):
375
+ name = statement.target.id
376
+ if name.startswith("_") or _is_class_var(statement.annotation):
377
+ continue
378
+ if name == "model_config":
379
+ model.proven = False
380
+ continue
381
+ model.fields.append(name)
382
+ alias = _declared_alias(statement.value)
383
+ if alias is not None:
384
+ model.aliases[name] = alias
385
+ elif isinstance(statement, ast.Assign):
386
+ for target in statement.targets:
387
+ if isinstance(target, ast.Name) and not target.id.startswith("_"):
388
+ if target.id == "model_config":
389
+ model.proven = False
390
+ else:
391
+ model.fields.append(target.id)
392
+ elif isinstance(statement, ast.ClassDef) and statement.name in {"Config", "model_config"}:
393
+ model.proven = False
394
+ return model
395
+
396
+
274
397
  class _ModuleVisitor(ast.NodeVisitor):
275
398
  """Collects routers, apps, imports, include edges, and route decorators.
276
399
 
@@ -321,6 +444,12 @@ class _ModuleVisitor(ast.NodeVisitor):
321
444
  )
322
445
  self.generic_visit(node)
323
446
 
447
+ # -- response models -----------------------------------------------------
448
+
449
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
450
+ self.index.models[node.name] = _model_def(node)
451
+ self.generic_visit(node)
452
+
324
453
  # -- functions: route decorators -----------------------------------------
325
454
 
326
455
  def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
@@ -394,7 +523,8 @@ class _ModuleVisitor(ast.NodeVisitor):
394
523
  if owner_ref is not None:
395
524
  owner_ref.routes.append(RouteDef(
396
525
  methods=[], path=None, node=node, file=self.index.relpath, handler=fn.name,
397
- is_async=is_async, response_model=None, request_schemas=[],
526
+ is_async=is_async, response_model=None,
527
+ effective_response_model=None, request_schemas=[],
398
528
  tags=[], operation_id=None,
399
529
  ))
400
530
  return
@@ -414,6 +544,9 @@ class _ModuleVisitor(ast.NodeVisitor):
414
544
  handler=fn.name,
415
545
  is_async=is_async,
416
546
  response_model=_dotted_name(_keyword(node, "response_model")),
547
+ effective_response_model=(
548
+ _model_name(_keyword(node, "response_model")) or _model_name(fn.returns)
549
+ ),
417
550
  request_schemas=_request_schema_names(fn, path),
418
551
  tags=[
419
552
  s for s in (
@@ -656,6 +789,94 @@ def _resolve_import(
656
789
  return ImportResolution(module=None)
657
790
 
658
791
 
792
+ class _ModelIndex:
793
+ """Resolves a response-model symbol to the wire names it answers to.
794
+
795
+ Bounded like every other channel here: the defining class must be in
796
+ the scanned set, reachable as a same-file definition or through one
797
+ resolved import, and every base class must resolve too. Anything the
798
+ AST pass cannot compute (an alias generator, an unresolvable base, an
799
+ unknown shape) yields ``None`` — the caller then reports no fields at
800
+ all rather than a partial list.
801
+ """
802
+
803
+ def __init__(self, indexes: dict[str, FileIndex], import_roots: tuple[str, ...] = ()) -> None:
804
+ self.models: dict[str, dict[str, ModelDef]] = {
805
+ relpath: index.models for relpath, index in indexes.items()
806
+ }
807
+ self.indexes = indexes
808
+ self.import_roots = import_roots
809
+ self.scanned: frozenset[str] = frozenset(indexes)
810
+ self.module_map = {_module_of(rel): rel for rel in indexes}
811
+
812
+ def fields_for(self, relpath: str, model: str | None) -> list[str] | None:
813
+ """Every wire name one response model answers to, or None."""
814
+ if model is None or model == "":
815
+ return None
816
+ name = model
817
+ prefix = ""
818
+ if "." in model:
819
+ prefix, name = model.rsplit(".", 1)
820
+ if prefix not in self.module_map:
821
+ return None
822
+ located = self._locate(relpath, name)
823
+ if located is None:
824
+ return None
825
+ found = self._collect(relpath, name, set())
826
+ if found is None or not found:
827
+ return None
828
+ return found
829
+
830
+ def _locate(self, relpath: str, name: str) -> ModelDef | None:
831
+ """The class definition a name refers to, or None."""
832
+ own = self.models.get(relpath, {}).get(name)
833
+ if own is not None:
834
+ return own
835
+ ref = self.indexes.get(relpath)
836
+ if ref is None:
837
+ return None
838
+ imported = ref.imports.get(name)
839
+ if imported is None or imported.name is None:
840
+ return None
841
+ resolution = _resolve_import(
842
+ relpath,
843
+ (imported.module, imported.level, imported.name),
844
+ self.module_map,
845
+ self.import_roots,
846
+ self.scanned,
847
+ )
848
+ if resolution.module is None:
849
+ return None
850
+ target = self.module_map.get(resolution.module)
851
+ return None if target is None else self.models.get(target, {}).get(name)
852
+
853
+ def _collect(self, relpath: str, name: str, seen: set[tuple[str, str]]) -> list[str] | None:
854
+ """Field and alias names of one model plus its provable bases."""
855
+ key = (relpath, name)
856
+ if key in seen:
857
+ return None
858
+ seen.add(key)
859
+ located = self._locate(relpath, name)
860
+ if located is None or not located.proven:
861
+ return None
862
+ names: list[str] = []
863
+ for base in located.bases:
864
+ if base in _FIELDLESS_BASES:
865
+ continue
866
+ if base == "?":
867
+ return None
868
+ inherited = self._collect(relpath, base, seen)
869
+ if inherited is None:
870
+ return None
871
+ names.extend(inherited)
872
+ for field in located.fields:
873
+ alias = located.aliases.get(field)
874
+ for value in (alias, field) if alias is not None else (field,):
875
+ if value not in names:
876
+ names.append(value)
877
+ return names
878
+
879
+
659
880
  class _Resolver:
660
881
  """Composes effective mounted paths over the cross-file mount graph."""
661
882
 
@@ -664,6 +885,7 @@ class _Resolver:
664
885
  self.import_roots = import_roots
665
886
  self.scanned: frozenset[str] = frozenset(indexes)
666
887
  self.module_map = {_module_of(rel): rel for rel in indexes}
888
+ self.models = _ModelIndex(indexes, import_roots)
667
889
  self.facts: list[dict] = []
668
890
  self.unresolved: list[dict] = []
669
891
 
@@ -1257,33 +1479,56 @@ class _Resolver:
1257
1479
  continue
1258
1480
  for method in route.methods:
1259
1481
  self.facts.append(
1260
- _fact(route.file, route, method, prefix + route.path, mount)
1482
+ _fact(route.file, route, method, prefix + route.path, mount, self.models)
1261
1483
  )
1262
1484
 
1263
1485
 
1264
- def _fact(relpath: str, route: RouteDef, method: str, effective_path: str, mount: str) -> dict:
1486
+ def _fact(
1487
+ relpath: str,
1488
+ route: RouteDef,
1489
+ method: str,
1490
+ effective_path: str,
1491
+ mount: str,
1492
+ models: _ModelIndex,
1493
+ ) -> dict:
1265
1494
  handler_qname = f"{relpath[:-3].replace('/', '.')}:{route.handler}"
1495
+ attributes = {
1496
+ "role": "server-route",
1497
+ "method": method,
1498
+ "rawPath": route.path or "",
1499
+ "normalizedPath": "", # canonicalized by the TS wrapper (single impl)
1500
+ "effectivePath": effective_path,
1501
+ "framework": FRAMEWORK,
1502
+ "handlerSymbol": handler_qname,
1503
+ "isAsync": route.is_async,
1504
+ "responseModel": route.response_model,
1505
+ "requestSchemaSymbols": route.request_schemas,
1506
+ "tags": route.tags,
1507
+ "operationId": route.operation_id,
1508
+ "mountProvenance": mount,
1509
+ }
1510
+ # Wire names the response model answers to (plan 2026-09-25 Phase 4b
1511
+ # item 5). Absent whenever the model is not statically provable — an
1512
+ # unresolvable symbol, an alias generator, an unprovable base, a
1513
+ # non-model shape — so the frontend-read check has nothing to compare
1514
+ # against and stays silent.
1515
+ fields = models.fields_for(route.file, route.effective_response_model)
1516
+ if fields is not None:
1517
+ # FastAPI answers a FAILED request with ``{"detail": ...}``
1518
+ # (HTTPException) or a ``detail`` list (request validation), and a
1519
+ # frontend reads it to report the error. It is therefore a
1520
+ # legitimate field of every route this pack reports, never
1521
+ # evidence of a dropped success-model field.
1522
+ if "detail" not in fields:
1523
+ fields.append("detail")
1524
+ attributes["responseModelFields"] = fields
1266
1525
  return {
1267
1526
  "schemaVersion": 1,
1268
1527
  "kind": CONTRACT_KIND,
1269
1528
  "source": relpath,
1270
1529
  "location": loc(relpath, route.node),
1271
1530
  "detectorVersion": VERSION,
1272
- "attributes": {
1273
- "role": "server-route",
1274
- "method": method,
1275
- "rawPath": route.path or "",
1276
- "normalizedPath": "", # canonicalized by the TS wrapper (single impl)
1277
- "effectivePath": effective_path,
1278
- "framework": FRAMEWORK,
1279
- "handlerSymbol": handler_qname,
1280
- "isAsync": route.is_async,
1281
- "responseModel": route.response_model,
1282
- "requestSchemaSymbols": route.request_schemas,
1283
- "tags": route.tags,
1284
- "operationId": route.operation_id,
1285
- "mountProvenance": mount,
1286
- },
1531
+ "attributes": attributes,
1287
1532
  "id": f"http.contract:{relpath}:{route.handler}:{method}:{effective_path}",
1288
1533
  }
1289
1534