sourcecode 2.5.3__py3-none-any.whl → 2.5.4__py3-none-any.whl

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.
@@ -130,9 +130,15 @@ _ANN_WITH_ARGS_RE = re.compile(
130
130
 
131
131
  _CLASS_DECL_RE = re.compile(
132
132
  r'(?:^|(?<=\s))'
133
- r'(?P<kind>class|interface|enum|@interface)\s+'
133
+ r'(?P<kind>class|interface|enum|record|@interface)\s+'
134
134
  r'(?P<name>[A-Z]\w*)'
135
135
  r'(?:\s*<[^{;]*?(?=>|\{))?'
136
+ # P1-F: record component list `record Point(int x, int y)`. `[^{;]*` is greedy
137
+ # to the LAST ')' before the body brace, so annotated components with
138
+ # argument parens (`@JsonProperty("x") String name`) stay inside the group.
139
+ # `record` is a contextual keyword: requiring `Name(` + a terminal '{' keeps
140
+ # identifiers named record (variables, methods) from matching.
141
+ r'(?:\s*\((?P<components>[^{;]*)\))?'
136
142
  r'(?:\s+extends\s+(?P<extends>[\w.<>?,\s]+?))?'
137
143
  r'(?:\s+implements\s+(?P<implements>[\w.<>?,\s]+?))?'
138
144
  r'(?:\s+permits\s+[\w,\s]+?)?'
@@ -145,7 +151,7 @@ _CLASS_DECL_RE = re.compile(
145
151
  # edges the impact graph needs. `[^{;]*?` keeps the match within a single declaration
146
152
  # head (stops at the body `{` or a `;`), matching the precision of _CLASS_DECL_RE.
147
153
  _INHERIT_PRESCAN_RE = re.compile(
148
- r'\b(?:class|interface)\s+[A-Z]\w*[^{;]*?\b(?:extends|implements)\b'
154
+ r'\b(?:class|interface|record)\s+[A-Z]\w*[^{;]*?\b(?:extends|implements)\b'
149
155
  )
150
156
 
151
157
  _METHOD_DECL_RE = re.compile(
@@ -197,6 +203,84 @@ _JAXRS_HTTP_ANNOTATIONS: frozenset[str] = frozenset({
197
203
  # Annotations whose args contain a path value and must be captured.
198
204
  _PATH_ANNOTATIONS: frozenset[str] = frozenset({"@Path"})
199
205
 
206
+ # P1-E cheap pre-filter: only a zero-symbol file containing a type keyword
207
+ # anywhere pays the linear mask + declaration check (reconciliation rule).
208
+ _TYPE_DECL_KEYWORD_RE = re.compile(r"\b(?:class|interface|enum|record)\b")
209
+
210
+ # P1-G: an inline-CLOSED block comment inside a declaration line.
211
+ _INLINE_BLOCK_COMMENT_RE = re.compile(r"/\*.*?\*/")
212
+
213
+ # SIM-1: `return new X(...)` inside a method body — the concrete type a locator
214
+ # method instantiates when its DECLARED return type is unusable (`Object`).
215
+ # Tolerates a package-qualified head (`return new org.x.Y(...)`).
216
+ _RETURN_NEW_RE = re.compile(r"\breturn\s+new\s+(?:[a-z_]\w*\.)*([A-Z]\w*)\s*[(<]")
217
+
218
+
219
+ def _note_return_new(annotation_values: dict, text: str) -> None:
220
+ """Accumulate distinct instantiated-return types on a method's annotation_values."""
221
+ for _rm in _RETURN_NEW_RE.finditer(text):
222
+ _t = _rm.group(1)
223
+ _prev = annotation_values.get("_returns_new", "")
224
+ _seen = _prev.split(",") if _prev else []
225
+ if _t not in _seen:
226
+ annotation_values["_returns_new"] = ",".join(_seen + [_t])
227
+
228
+
229
+ def _scan_line_comments(line: str, in_block: bool) -> tuple[str, bool]:
230
+ """Remove block-comment spans from one line, STRING-LITERAL AWARE (P1-G).
231
+
232
+ Naive `"/*" in line` checks misread comment markers inside string literals
233
+ (`@Consumes("*/*")`, eureka DiscoveryJerseyProvider) and poison the block-
234
+ comment state for the rest of the file. This scanner tracks quotes so
235
+ markers inside strings/chars are code, drops `//` tails, and returns the
236
+ residual code text plus the new in-block state. Callers fast-path: only a
237
+ line containing a marker (or scanned while inside a block) pays this loop.
238
+ """
239
+ out: list[str] = []
240
+ i, n = 0, len(line)
241
+ in_str = in_chr = False
242
+ while i < n:
243
+ c = line[i]
244
+ if in_block:
245
+ if c == "*" and i + 1 < n and line[i + 1] == "/":
246
+ in_block = False
247
+ i += 2
248
+ continue
249
+ i += 1
250
+ continue
251
+ if in_str or in_chr:
252
+ out.append(c)
253
+ if c == "\\" and i + 1 < n:
254
+ out.append(line[i + 1])
255
+ i += 2
256
+ continue
257
+ if in_str and c == '"':
258
+ in_str = False
259
+ elif in_chr and c == "'":
260
+ in_chr = False
261
+ i += 1
262
+ continue
263
+ if c == '"':
264
+ in_str = True
265
+ out.append(c)
266
+ i += 1
267
+ continue
268
+ if c == "'":
269
+ in_chr = True
270
+ out.append(c)
271
+ i += 1
272
+ continue
273
+ if c == "/" and i + 1 < n:
274
+ if line[i + 1] == "/":
275
+ break # line comment — rest of the line is not code
276
+ if line[i + 1] == "*":
277
+ in_block = True
278
+ i += 2
279
+ continue
280
+ out.append(c)
281
+ i += 1
282
+ return "".join(out), in_block
283
+
200
284
  # Security / authorization annotations whose args must be captured.
201
285
  # Includes standard Jakarta EE, JAX-RS, Quarkus/MicroProfile, and custom patterns.
202
286
  _PERMISSION_ANNOTATIONS: frozenset[str] = frozenset({
@@ -563,7 +647,7 @@ _ANN_PREFIX_RE = re.compile(r'^(?:@\w+\s*(?:\([^)]*\))?\s*)+')
563
647
  _STRING_LITERAL_RE = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
564
648
 
565
649
  # Module-level cache for class-keyword detection (avoids recompilation per _extract_symbols call)
566
- _CLASS_KW_RE = re.compile(r'\b(?:class|interface|enum)\s+[A-Z]')
650
+ _CLASS_KW_RE = re.compile(r'\b(?:class|interface|enum|record)\s+[A-Z]')
567
651
 
568
652
 
569
653
  # ---------------------------------------------------------------------------
@@ -774,6 +858,9 @@ def _extract_symbols(
774
858
  pending_anns: list[str] = []
775
859
  pending_ann_values: dict[str, str] = {}
776
860
  in_block_comment = False
861
+ # SIM-1: (symbol index, declaration-line depth) of the method whose body the
862
+ # scan is currently inside — target for `return new X(...)` attribution.
863
+ _ret_new_owner: Optional[tuple[int, int]] = None
777
864
 
778
865
  # BUG-PARSER-001: normalize multi-line class declarations where the opening brace
779
866
  # appears on a continuation line (e.g. "implements A,\n B, C {").
@@ -837,14 +924,18 @@ def _extract_symbols(
837
924
  cur_line = _normalized_src_lines[_ni] if _ni < len(_normalized_src_lines) else None
838
925
  stripped = line.strip()
839
926
 
840
- if in_block_comment:
841
- if "*/" in stripped:
842
- in_block_comment = False
843
- continue
844
- if "/*" in stripped:
845
- if "*/" not in stripped:
846
- in_block_comment = True
927
+ # P1-G: comment handling. Old code dropped ANY line containing a block-
928
+ # comment marker, silently erasing declarations glued to comments
929
+ # (`class Proc /* impl */{`, `*/public class X {`) — and a naive marker
930
+ # check misreads markers inside string literals (`@Consumes("*/*")`).
931
+ # Fast path: a line with no marker while outside a block skips the scan.
932
+ if in_block_comment and "*/" not in stripped:
847
933
  continue
934
+ if in_block_comment or "/*" in stripped:
935
+ stripped, in_block_comment = _scan_line_comments(stripped, in_block_comment)
936
+ stripped = stripped.strip()
937
+ if not stripped:
938
+ continue
848
939
  if stripped.startswith("//") or stripped.startswith("*"):
849
940
  continue
850
941
 
@@ -878,27 +969,38 @@ def _extract_symbols(
878
969
  modifiers = _extract_modifiers(stripped[:cls_m.start()])
879
970
  extends_str = (cls_m.group("extends") or "").strip()
880
971
  implements_str = (cls_m.group("implements") or "").strip()
972
+ # P1-F: record components carry the type's state — their types are
973
+ # real dependencies (imports_used) and the list belongs in the
974
+ # signature so consumers see the shape without re-reading source.
975
+ components_str = (cls_m.group("components") or "").strip()
881
976
 
882
977
  sig_types = ([extends_str] if extends_str else []) + (
883
978
  [s.strip() for s in implements_str.split(",") if s.strip()]
884
979
  if implements_str else []
885
980
  )
886
- used = _resolve_types_from_text(" ".join(sig_types), import_map)
981
+ used = _resolve_types_from_text(
982
+ " ".join(sig_types) + (" " + components_str if components_str else ""),
983
+ import_map,
984
+ )
887
985
 
888
986
  sym_type = "interface" if kind_kw == "interface" else "class"
889
987
 
890
- # symbol_kind distinguishes enum/annotation from class/interface
988
+ # symbol_kind distinguishes enum/annotation/record from class/interface
891
989
  if kind_kw == "enum":
892
990
  sym_kind = "enum"
893
991
  elif kind_kw == "@interface":
894
992
  sym_kind = "annotation"
895
993
  elif kind_kw == "interface":
896
994
  sym_kind = "interface"
995
+ elif kind_kw == "record":
996
+ sym_kind = "record"
897
997
  else:
898
998
  sym_kind = "class"
899
999
 
900
1000
  _stable_id = _compute_stable_id(package, name, sym_kind, name)
901
1001
  _sig_parts = [kind_kw, name]
1002
+ if components_str:
1003
+ _sig_parts.append(f"({components_str})")
902
1004
  if extends_str:
903
1005
  _sig_parts.append(f"extends {extends_str}")
904
1006
  if implements_str:
@@ -1008,6 +1110,10 @@ def _extract_symbols(
1008
1110
  ))
1009
1111
  pending_anns = []
1010
1112
  pending_ann_values = {}
1113
+ # SIM-1: remember this method as the enclosing body for
1114
+ # `return new X(...)` attribution (locator child binding).
1115
+ _ret_new_owner = (len(symbols) - 1, depth)
1116
+ _note_return_new(symbols[-1].annotation_values, stripped)
1011
1117
  depth += net
1012
1118
  _pop_closed(class_stack, depth)
1013
1119
  continue
@@ -1089,6 +1195,15 @@ def _extract_symbols(
1089
1195
  _pop_closed(class_stack, depth)
1090
1196
  continue
1091
1197
 
1198
+ # SIM-1: attribute `return new X(...)` body statements to the enclosing
1199
+ # method. Owner is cleared as soon as a line at (or above) the method's
1200
+ # declaration depth is seen — the method body has closed.
1201
+ if _ret_new_owner is not None:
1202
+ if depth <= _ret_new_owner[1]:
1203
+ _ret_new_owner = None
1204
+ elif "return new" in stripped:
1205
+ _note_return_new(symbols[_ret_new_owner[0]].annotation_values, stripped)
1206
+
1092
1207
  pending_anns = []
1093
1208
  pending_ann_values = {}
1094
1209
  depth += net
@@ -1243,7 +1358,7 @@ _ANN_REF_TOKEN_RE = re.compile(r'\b[A-Z]\w*(?:\.\w+)+')
1243
1358
  # the class whose body encloses them.
1244
1359
  _ANN_OR_DECL_OR_BRACE_RE = re.compile(
1245
1360
  r'@([A-Z]\w*)\s*\('
1246
- r'|\b(?:class|interface|enum)\s+([A-Z]\w*)'
1361
+ r'|\b(?:class|interface|enum|record)\s+([A-Z]\w*)'
1247
1362
  r'|([{}])'
1248
1363
  )
1249
1364
 
@@ -2016,6 +2131,38 @@ def _file_import_map(source: str) -> dict[str, str]:
2016
2131
  return out
2017
2132
 
2018
2133
 
2134
+ def _record_component_pairs(components: str) -> list[tuple[str, str]]:
2135
+ """(name, declared_type) pairs from a record component list (P1-F).
2136
+
2137
+ Depth-aware comma split so generic commas (`Map<String, X> m`) and
2138
+ annotation-argument parens don't break components apart; leading
2139
+ annotations stripped; varargs normalized to arrays.
2140
+ """
2141
+ parts: list[str] = []
2142
+ buf: list[str] = []
2143
+ d = 0
2144
+ for ch in components:
2145
+ if ch in "<(":
2146
+ d += 1
2147
+ elif ch in ">)":
2148
+ d -= 1
2149
+ if ch == "," and d == 0:
2150
+ parts.append("".join(buf))
2151
+ buf = []
2152
+ else:
2153
+ buf.append(ch)
2154
+ if buf:
2155
+ parts.append("".join(buf))
2156
+ out: list[tuple[str, str]] = []
2157
+ for p in parts:
2158
+ p = re.sub(r"@[\w.]+(?:\([^)]*\))?\s*", "", p).strip()
2159
+ p = p.replace("...", "[] ")
2160
+ toks = p.rsplit(None, 1)
2161
+ if len(toks) == 2 and toks[1].isidentifier():
2162
+ out.append((toks[1], toks[0].strip()))
2163
+ return out
2164
+
2165
+
2019
2166
  def _extract_field_types(
2020
2167
  source: str, *, imports: Optional[dict[str, str]] = None
2021
2168
  ) -> dict[str, dict[str, str]]:
@@ -2044,14 +2191,14 @@ def _extract_field_types(
2044
2191
  in_block = False
2045
2192
  for raw in source.splitlines():
2046
2193
  stripped = raw.strip()
2047
- if in_block:
2048
- if "*/" in stripped:
2049
- in_block = False
2050
- continue
2051
- if stripped.startswith("/*"):
2052
- if "*/" not in stripped:
2053
- in_block = True
2194
+ # P1-G: string-literal-aware comment handling (see _extract_symbols).
2195
+ if in_block and "*/" not in stripped:
2054
2196
  continue
2197
+ if in_block or "/*" in stripped:
2198
+ stripped, in_block = _scan_line_comments(stripped, in_block)
2199
+ stripped = stripped.strip()
2200
+ if not stripped:
2201
+ continue
2055
2202
  if not stripped or stripped.startswith("//") or stripped.startswith("*"):
2056
2203
  continue
2057
2204
  if stripped.startswith("@"):
@@ -2073,6 +2220,16 @@ def _extract_field_types(
2073
2220
  fqn = f"{class_stack[-1][0]}.{name}" if class_stack else (
2074
2221
  f"{package}.{name}" if package else name)
2075
2222
  class_stack.append((fqn, depth))
2223
+ # P1-F: record components are implicit final fields — register them
2224
+ # so a `component.method()` receiver binds to its declared type the
2225
+ # same way an ordinary injected field does.
2226
+ _comps = (cls_m.group("components") or "").strip()
2227
+ if cls_m.group("kind") == "record" and _comps:
2228
+ for _cn, _ct in _record_component_pairs(_comps):
2229
+ _outer, _ = _type_tokens(_ct)
2230
+ if _cn and _outer and _cn not in _JAVA_KEYWORDS:
2231
+ out.setdefault(fqn, {}).setdefault(
2232
+ _cn, imports.get(_outer, _outer))
2076
2233
  depth += net
2077
2234
  _pop_closed(class_stack, depth)
2078
2235
  continue
@@ -2865,14 +3022,22 @@ def _build_jaxrs_locator_map(symbols: list["SymbolRecord"]) -> dict[str, list[tu
2865
3022
  locator_path = _parse_route_path(sym.annotation_values.get("@Path", ""))
2866
3023
 
2867
3024
  ret = sym.return_type.strip()
2868
- if not ret:
2869
- continue
2870
3025
  # Simplify: strip package prefix and generics
2871
- ret_simple = ret.split(".")[-1].split("<")[0].strip()
2872
- if not ret_simple or ret_simple in _LOCATOR_SKIP_RETURN_TYPES:
2873
- continue
2874
- if ret_simple[0].islower(): # lowercase primitive/variable, not a class
2875
- continue
3026
+ ret_simple = ret.split(".")[-1].split("<")[0].strip() if ret else ""
3027
+ if (not ret_simple or ret_simple in _LOCATOR_SKIP_RETURN_TYPES
3028
+ or ret_simple[0].islower()):
3029
+ # SIM-1: JAX-RS locators commonly declare `Object` while the body
3030
+ # instantiates the concrete sub-resource (keycloak
3031
+ # `@Path("auth") public Object auth() { return new
3032
+ # AuthorizationEndpoint(...); }`). When the declared type is
3033
+ # unusable, bind through the instantiated type — but only when it
3034
+ # is UNAMBIGUOUS (exactly one distinct `return new` type; a
3035
+ # dynamic/multi-branch locator stays unresolved, honestly).
3036
+ _rn = sym.annotation_values.get("_returns_new", "")
3037
+ _cands = [t for t in _rn.split(",") if t] if _rn else []
3038
+ if len(set(_cands)) != 1 or _cands[0] in _LOCATOR_SKIP_RETURN_TYPES:
3039
+ continue
3040
+ ret_simple = _cands[0]
2876
3041
 
2877
3042
  parent_fqn = _enclosing_class(sym.symbol)
2878
3043
  parent_simple = parent_fqn.split(".")[-1]
@@ -2904,27 +3069,40 @@ def _resolve_jaxrs_prefixes(
2904
3069
  return own_prefixes
2905
3070
 
2906
3071
  new_visited = visited | {cls_simple}
2907
- full_prefixes: list[str] = []
3072
+ # Rooted = composed through a parent that has a resolvable prefix (or a
3073
+ # class-level @Path). Unrooted = the parent chain dead-ends in a class with
3074
+ # no static mount point (keycloak: OIDCLoginProtocolService is mounted via a
3075
+ # fully dynamic `getProtocol` SPI locator).
3076
+ rooted_prefixes: list[str] = []
3077
+ unrooted_prefixes: list[str] = []
2908
3078
 
2909
3079
  for parent_simple, locator_path in locator_map[cls_simple]:
2910
3080
  parent_full = _resolve_jaxrs_prefixes(parent_simple, class_info, locator_map, new_visited)
2911
- # Skip implementation/unrooted parents: if the parent resolves to only empty
2912
- # prefixes AND has no class-level @Path annotation, it is a concrete impl class
2913
- # (e.g. DefaultClientsApi implements ClientsApi) that duplicates a locator method
2914
- # from its interface. Including it would produce spurious short paths like /{id}
2915
- # alongside the correctly-resolved full path. The interface version is already
2916
- # in the locator_map and will produce the correct full path.
3081
+ # Impl/unrooted parent split: a parent resolving to only empty prefixes
3082
+ # with no class-level @Path is either a concrete impl class duplicating
3083
+ # its interface's locator (e.g. DefaultClientsApi implements ClientsApi)
3084
+ # or a genuinely unrooted chain. When ANY rooted composition exists it
3085
+ # wins and the unrooted ones are dropped (the impl-duplicate case, as
3086
+ # before). When NONE exists, the RELATIVE composition is still far more
3087
+ # truthful than collapsing 39 endpoints to path "/" (SIM-1): emit
3088
+ # locator_path + own path with the unresolved head simply absent.
2917
3089
  _parent_has_path_ann = class_info.get(parent_simple, {}).get("has_path_ann", False)
2918
3090
  _non_empty_parent = [p for p in parent_full if p]
2919
- if not _non_empty_parent and not _parent_has_path_ann:
2920
- continue
2921
3091
  use_parent_paths = _non_empty_parent if _non_empty_parent else parent_full
3092
+ _bucket = (
3093
+ rooted_prefixes if (_non_empty_parent or _parent_has_path_ann)
3094
+ else unrooted_prefixes
3095
+ )
2922
3096
  for pp in use_parent_paths:
2923
3097
  for op in own_prefixes:
2924
3098
  combined = _join_path_segments(pp, locator_path, op)
2925
- full_prefixes.append(combined)
3099
+ _bucket.append(combined)
2926
3100
 
2927
- return full_prefixes if full_prefixes else own_prefixes
3101
+ if rooted_prefixes:
3102
+ return rooted_prefixes
3103
+ if unrooted_prefixes:
3104
+ return unrooted_prefixes
3105
+ return own_prefixes
2928
3106
 
2929
3107
 
2930
3108
  # ---------------------------------------------------------------------------
@@ -3650,6 +3828,7 @@ def _compute_analysis_gaps(
3650
3828
  spring_summary: dict,
3651
3829
  route_surface: list[dict],
3652
3830
  relations: list[RelationEdge],
3831
+ unparsed_type_files: Optional[list[str]] = None,
3653
3832
  ) -> list[dict]:
3654
3833
  """Compute structural analysis gaps — real system failures, not cosmetic issues."""
3655
3834
  gaps: list[dict] = []
@@ -3662,6 +3841,26 @@ def _compute_analysis_gaps(
3662
3841
  })
3663
3842
  return gaps
3664
3843
 
3844
+ # P1-E: parse-coverage reconciliation (file inventory × symbol index). A file
3845
+ # that declares a type but produced zero symbols is silently invisible to the
3846
+ # graph, callers and endpoints — an agent reading "0 callers" cannot know the
3847
+ # defining or calling code was never modeled unless this gap says so.
3848
+ if unparsed_type_files:
3849
+ from sourcecode.reconciliation import reconcile_extraction_evidence
3850
+ extracted = frozenset(s.declaring_file for s in symbols)
3851
+ for rf in reconcile_extraction_evidence(
3852
+ type_decl_files=frozenset(unparsed_type_files),
3853
+ extracted_files=extracted,
3854
+ ):
3855
+ _sample = list(rf.symbols[:5])
3856
+ gaps.append({
3857
+ "area": rf.rule,
3858
+ "reason": rf.detail + " Files: " + ", ".join(_sample)
3859
+ + (", …" if len(rf.symbols) > 5 else ""),
3860
+ "impact": "high",
3861
+ "files": list(rf.symbols),
3862
+ })
3863
+
3665
3864
  controllers = spring_summary.get("controllers", [])
3666
3865
  if controllers and not route_surface:
3667
3866
  gaps.append({
@@ -3712,6 +3911,7 @@ def _assemble(
3712
3911
  spring_summary: dict, # noqa: ARG001 — used internally via _spring_role on symbols
3713
3912
  route_diffs: list[dict] | None = None,
3714
3913
  custom_security: "tuple[CustomSecuritySpec, ...]" = (),
3914
+ unparsed_type_files: Optional[list[str]] = None,
3715
3915
  ) -> dict:
3716
3916
  """Phase 5: Final assembly — single deterministic output contract."""
3717
3917
  sorted_syms = sorted(symbols, key=lambda s: s.symbol)
@@ -3976,7 +4176,10 @@ def _assemble(
3976
4176
  _route_surface = _build_route_surface(
3977
4177
  sorted_syms, route_diffs, extends_map=_extends_map, custom_security=custom_security
3978
4178
  )
3979
- _analysis_gaps = _compute_analysis_gaps(sorted_syms, spring_summary, _route_surface, sorted_rels)
4179
+ _analysis_gaps = _compute_analysis_gaps(
4180
+ sorted_syms, spring_summary, _route_surface, sorted_rels,
4181
+ unparsed_type_files=unparsed_type_files,
4182
+ )
3980
4183
 
3981
4184
  # Detect filter-based security model for the assembled IR.
3982
4185
  # Stored here so CIR projections (project_endpoint_surface) can read it without
@@ -4676,6 +4879,9 @@ def build_repo_ir(
4676
4879
  # (package, imports, annotation-ref-pairs) for the reference-graph post-pass,
4677
4880
  # collected from BOTH fully-parsed and pre-scan-skipped files (BUG #2).
4678
4881
  _ann_ref_sources: list[tuple[str, list[str], list[tuple[str, str]]]] = []
4882
+ # P1-E: files whose masked source declares a type but extraction yielded
4883
+ # zero symbols — fed to the parse_coverage reconciliation rule at assembly.
4884
+ _unparsed_type_files: list[str] = []
4679
4885
  for rel_path in sorted(file_paths):
4680
4886
  abs_path = root / rel_path
4681
4887
  try:
@@ -4689,6 +4895,15 @@ def build_repo_ir(
4689
4895
  source, rel_path, extra_capture=_extra_capture
4690
4896
  )
4691
4897
  all_symbols.extend(symbols)
4898
+ # P1-E (parse-coverage reconciliation): a file that declares a type but
4899
+ # yielded zero symbols is silently invisible to the graph. Record the
4900
+ # candidate here (source is already in memory; masking only runs for the
4901
+ # rare zero-symbol file) — the reconciliation rule turns the set into an
4902
+ # analysis_gaps entry at assembly.
4903
+ if not symbols and _TYPE_DECL_KEYWORD_RE.search(source):
4904
+ from sourcecode.reconciliation import declares_java_type
4905
+ if declares_java_type(_mask_java(source)):
4906
+ _unparsed_type_files.append(rel_path)
4692
4907
  _per_file.append((rel_path, source, package, raw_imports, symbols))
4693
4908
  if "@" in source:
4694
4909
  _parsed_refs = _extract_annotation_type_refs(source, package)
@@ -4806,6 +5021,7 @@ def build_repo_ir(
4806
5021
  ir = _assemble(
4807
5022
  all_symbols, unique_relations, all_changed, spring_summary, route_diffs_arg,
4808
5023
  custom_security=_custom_sec_tuple,
5024
+ unparsed_type_files=_unparsed_type_files,
4809
5025
  )
4810
5026
 
4811
5027
  # Fase 21: merge OpenAPI-spec-recovered routes for interface-defined controllers