codegraph-brain 0.23.0__py3-none-any.whl → 0.23.2__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.
cgis/api/mcp_server.py CHANGED
@@ -966,8 +966,9 @@ def cgis_audit_reachability(
966
966
  str | None,
967
967
  Field(
968
968
  description="Only audit sources at or under this FQN prefix, matched on whole "
969
- "dot-segments — a partial segment selects nothing and returns an empty audit, "
970
- "not a clean one. Combined with from_type when both are given."
969
+ "dot-segments. A selection matching no source is an error naming the "
970
+ "whole-segment prefixes it may have meant. Combined with from_type when both "
971
+ "are given."
971
972
  ),
972
973
  ] = None,
973
974
  depth: Annotated[
@@ -986,6 +987,9 @@ def cgis_audit_reachability(
986
987
  one is required. Returns JSON ``{target, covered, gaps}`` where each gap carries
987
988
  ``fqn``/``file``/``line``. Generalizes to validators, event tracking, or
988
989
  service-layer-boundary rules by pointing ``target`` at the required node.
990
+
991
+ A selection that matches no source returns a ❌ message, not an empty
992
+ ``{covered: [], gaps: []}`` that would read as a passing audit (#467).
989
993
  """
990
994
  if blank := _blank_fqn_error(target):
991
995
  return blank
cgis/cli.py CHANGED
@@ -20,6 +20,7 @@ style tags you wrote yourself and pure numbers need no escaping.
20
20
 
21
21
  import dataclasses
22
22
  import json as _json
23
+ from collections.abc import Callable
23
24
  from enum import StrEnum
24
25
  from pathlib import Path
25
26
 
@@ -42,7 +43,7 @@ from cgis.query.analysis.analyzer import AnalyzerEngine
42
43
  from cgis.query.analysis.anomaly import AnomalyType, ArchitecturalAnomaly
43
44
  from cgis.query.analysis.health import HealthScorer
44
45
  from cgis.query.analysis.suggest_service import SuggestReport, report_to_dict, suggest_packages
45
- from cgis.query.context.audit import ReachabilityAudit, audit_reachability
46
+ from cgis.query.context.audit import NoAuditSourcesError, ReachabilityAudit, audit_reachability
46
47
  from cgis.query.context.context_service import build_context
47
48
  from cgis.query.context.orphans import OrphanReport, find_orphan_classes
48
49
  from cgis.query.drift.drift import DriftReport, FitQuality
@@ -318,6 +319,40 @@ def _resolve_cli_fqn(store: SQLiteStore, target: str, kind: str) -> str:
318
319
  return resolution.resolved
319
320
 
320
321
 
322
+ def _tree_neighbours(
323
+ store: SQLiteStore,
324
+ edges: list[Edge],
325
+ neighbour_of: Callable[[Edge], str],
326
+ *,
327
+ allowed_edge_types: frozenset[EdgeType] | None,
328
+ show_external: bool,
329
+ min_confidence: float | None,
330
+ ) -> list[tuple[str, Node | None]]:
331
+ """The neighbours one tree level shows: filtered, in edge order, each id once.
332
+
333
+ One entry per neighbour, not per edge: two call sites are separate edges to the
334
+ same node, and a tree row shows neither line nor edge type to tell them apart
335
+ (#463). A neighbour absent from the store (an unresolved target) comes back with
336
+ ``None``, and is dropped with the non-INTERNAL ones when ``show_external`` is off.
337
+ """
338
+ if allowed_edge_types is not None:
339
+ edges = [e for e in edges if e.type in allowed_edge_types]
340
+ if min_confidence is not None:
341
+ edges = [e for e in edges if e.confidence >= min_confidence]
342
+ ids = list(dict.fromkeys(neighbour_of(e) for e in edges))
343
+ nodes = {n.id: n for n in store.get_nodes(ids)}
344
+ return [
345
+ (node_id, nodes.get(node_id))
346
+ for node_id in ids
347
+ if show_external or _is_internal(nodes.get(node_id))
348
+ ]
349
+
350
+
351
+ def _is_internal(node: Node | None) -> bool:
352
+ """True for a stored node from the ingested repository."""
353
+ return node is not None and node.namespace == NodeNamespace.INTERNAL
354
+
355
+
321
356
  def build_trace_tree(
322
357
  store: SQLiteStore,
323
358
  current_id: str,
@@ -333,21 +368,15 @@ def build_trace_tree(
333
368
  if current_depth >= max_depth:
334
369
  return
335
370
 
336
- outgoing = store.get_outgoing_edges(current_id)
337
- if allowed_edge_types is not None:
338
- outgoing = [e for e in outgoing if e.type in allowed_edge_types]
339
- if min_confidence is not None:
340
- outgoing = [e for e in outgoing if e.confidence >= min_confidence]
341
- nodes_map = {n.id: n for n in store.get_nodes([e.target for e in outgoing])}
342
- for edge in outgoing:
343
- target_id = edge.target
344
- target_node = nodes_map.get(target_id)
345
-
346
- if not show_external and (
347
- target_node is None or target_node.namespace != NodeNamespace.INTERNAL
348
- ):
349
- continue
350
-
371
+ callees = _tree_neighbours(
372
+ store,
373
+ store.get_outgoing_edges(current_id),
374
+ lambda edge: edge.target,
375
+ allowed_edge_types=allowed_edge_types,
376
+ show_external=show_external,
377
+ min_confidence=min_confidence,
378
+ )
379
+ for target_id, target_node in callees:
351
380
  if target_node:
352
381
  label = (
353
382
  f"[bold green]{target_node.type.value}[/bold green] "
@@ -470,21 +499,15 @@ def build_impact_tree(
470
499
  if current_depth >= max_depth:
471
500
  return
472
501
 
473
- incoming = store.get_incoming_edges(current_id)
474
- if allowed_edge_types is not None:
475
- incoming = [e for e in incoming if e.type in allowed_edge_types]
476
- if min_confidence is not None:
477
- incoming = [e for e in incoming if e.confidence >= min_confidence]
478
- nodes_map = {n.id: n for n in store.get_nodes([e.source for e in incoming])}
479
- for edge in incoming:
480
- source_id = edge.source
481
- source_node = nodes_map.get(source_id)
482
-
483
- if not show_external and (
484
- source_node is None or source_node.namespace != NodeNamespace.INTERNAL
485
- ):
486
- continue
487
-
502
+ callers = _tree_neighbours(
503
+ store,
504
+ store.get_incoming_edges(current_id),
505
+ lambda edge: edge.source,
506
+ allowed_edge_types=allowed_edge_types,
507
+ show_external=show_external,
508
+ min_confidence=min_confidence,
509
+ )
510
+ for source_id, source_node in callers:
488
511
  if source_node:
489
512
  label = (
490
513
  f"[bold magenta]{source_node.type.value}[/bold magenta] "
@@ -1445,7 +1468,7 @@ def audit(
1445
1468
  help="Only audit nodes of this type (e.g. ROUTE_HANDLER, API_ENDPOINT).",
1446
1469
  ),
1447
1470
  from_prefix: str | None = typer.Option(
1448
- None, "--from-prefix", help="Only audit nodes whose FQN starts with this prefix."
1471
+ None, "--from-prefix", help="Only audit nodes under this FQN prefix (whole dot-segments)."
1449
1472
  ),
1450
1473
  db: str = typer.Option(_DEFAULT_DB, "--db", "-d", help=_DEFAULT_DB_HELP),
1451
1474
  depth: int = typer.Option(
@@ -1487,13 +1510,19 @@ def audit(
1487
1510
 
1488
1511
  with SQLiteStore(db) as store:
1489
1512
  resolved = _resolve_checkpoint(store, target)
1490
- result = audit_reachability(
1491
- store,
1492
- target_fqn=resolved,
1493
- from_type=from_type,
1494
- from_prefix=from_prefix,
1495
- max_depth=depth,
1496
- )
1513
+ try:
1514
+ result = audit_reachability(
1515
+ store,
1516
+ target_fqn=resolved,
1517
+ from_type=from_type,
1518
+ from_prefix=from_prefix,
1519
+ max_depth=depth,
1520
+ )
1521
+ except NoAuditSourcesError as exc:
1522
+ # Exit 2 like a bad selector: exit 0 here let a typo'd prefix pass a CI gate (#467).
1523
+ # stderr, like _resolve_checkpoint: `--format json` stdout stays a clean payload.
1524
+ err_console.print(f"[bold red]❌ {escape(str(exc))}[/bold red]")
1525
+ raise typer.Exit(code=2) from exc
1497
1526
 
1498
1527
  if output_format == OutputFormat.JSON:
1499
1528
  typer.echo(_json.dumps(dataclasses.asdict(result), indent=2))
@@ -51,29 +51,100 @@ class ReachabilityAudit:
51
51
  gaps: list[NodeRef]
52
52
 
53
53
 
54
+ class NoAuditSourcesError(ValueError):
55
+ """The selectors matched no source to audit, so there is no result to report (#467).
56
+
57
+ Raised instead of returning empty ``covered``/``gaps``: callers — and CI gates
58
+ reading ``gaps == []`` — would otherwise take an audit of nothing for a clean one.
59
+ """
60
+
61
+
62
+ _SUGGESTION_LIMIT = 5
63
+
64
+
65
+ def _describe_selection(from_type: NodeType | None, from_prefix: str | None) -> str:
66
+ """Render the selectors the way a caller passed them, for the error message."""
67
+ parts = []
68
+ if from_type is not None:
69
+ parts.append(f"from_type={from_type.value}")
70
+ if from_prefix is not None:
71
+ parts.append(f"from_prefix={from_prefix!r}")
72
+ return " and ".join(parts)
73
+
74
+
54
75
  def _prefix_matches(fqn: str, prefix: str) -> bool:
55
76
  """Dot-boundary prefix match — ``app.routes`` matches ``app.routes.x``, not ``app.routesX``."""
56
77
  return fqn == prefix or fqn.startswith(f"{prefix}.")
57
78
 
58
79
 
80
+ def _is_candidate(node: Node, from_type: NodeType | None, target_fqn: str) -> bool:
81
+ """An INTERNAL node of the requested type that is not the checkpoint — a possible source."""
82
+ return (
83
+ node.namespace == NodeNamespace.INTERNAL
84
+ and (from_type is None or node.type == from_type)
85
+ and node.id != target_fqn
86
+ )
87
+
88
+
59
89
  def _select_sources(
60
- store: SQLiteStore, from_type: NodeType | None, from_prefix: str | None
90
+ nodes: list[Node], target_fqn: str, from_type: NodeType | None, from_prefix: str | None
61
91
  ) -> list[Node]:
62
92
  """Select INTERNAL source nodes by node type and/or dot-boundary FQN prefix (AND-combined).
63
93
 
64
94
  Restricted to INTERNAL nodes so the audit never flags stdlib/third-party
65
95
  code, and the prefix is boundary-aware so a name overlap (``app.routesX``)
66
- can't sneak into an ``app.routes`` selection.
96
+ can't sneak into an ``app.routes`` selection. The checkpoint is never its
97
+ own source.
67
98
  """
68
99
  return [
69
100
  node
70
- for node in store.get_all_nodes()
71
- if node.namespace == NodeNamespace.INTERNAL
72
- and (from_type is None or node.type == from_type)
101
+ for node in nodes
102
+ if _is_candidate(node, from_type, target_fqn)
73
103
  and (from_prefix is None or _prefix_matches(node.id, from_prefix))
74
104
  ]
75
105
 
76
106
 
107
+ def _empty_selection_hint(
108
+ nodes: list[Node], target_fqn: str, from_type: NodeType | None, from_prefix: str | None
109
+ ) -> str:
110
+ """Explain an empty selection: the whole-segment prefix meant, or what the prefix does hold.
111
+
112
+ Suggestions come from the same candidates ``_select_sources`` would accept, so
113
+ every suggested prefix selects at least one source. That also means the input
114
+ is never offered back: a candidate completing to ``from_prefix`` would itself
115
+ have been selected. A trailing dot is dropped first: ``app.routes.`` meant
116
+ ``app.routes``.
117
+ """
118
+ if from_prefix is None:
119
+ return ""
120
+ stem = from_prefix.rstrip(".")
121
+ suggestions: set[str] = set()
122
+ for node in nodes:
123
+ if _is_candidate(node, from_type, target_fqn) and node.id.startswith(stem):
124
+ next_dot = node.id.find(".", len(stem))
125
+ suggestions.add(node.id if next_dot == -1 else node.id[:next_dot])
126
+ if suggestions:
127
+ listing = ", ".join(sorted(suggestions)[:_SUGGESTION_LIMIT])
128
+ return f"Prefixes match whole dot-segments; did you mean: {listing}?"
129
+ under = [
130
+ n for n in nodes if n.namespace == NodeNamespace.INTERNAL and _prefix_matches(n.id, stem)
131
+ ]
132
+ if under and all(n.id == target_fqn for n in under):
133
+ return "Only the checkpoint itself is under that prefix, and it is never its own source."
134
+ if under and from_type is not None:
135
+ return f"There are nodes under that prefix, but none of type {from_type.value}."
136
+ return ""
137
+
138
+
139
+ def _no_sources_error(
140
+ nodes: list[Node], target_fqn: str, from_type: NodeType | None, from_prefix: str | None
141
+ ) -> NoAuditSourcesError:
142
+ """Build the error for an empty selection, with a hint when one can be given."""
143
+ msg = f"No sources matched {_describe_selection(from_type, from_prefix)} — nothing was audited."
144
+ hint = _empty_selection_hint(nodes, target_fqn, from_type, from_prefix)
145
+ return NoAuditSourcesError(f"{msg} {hint}" if hint else msg)
146
+
147
+
77
148
  def _ref(node: Node) -> NodeRef:
78
149
  """Build a located reference from a node."""
79
150
  return NodeRef(fqn=node.id, file=node.file_path, line=node.start_line)
@@ -112,11 +183,20 @@ def audit_reachability(
112
183
  already-injected instance — so a dependency-injected collaborator looks like
113
184
  a gap. Target the specific method (or the constructor) when auditing "does X
114
185
  use this collaborator".
186
+
187
+ Raises:
188
+ NoAuditSourcesError: The selectors match no source other than the checkpoint
189
+ itself. An empty ``covered``/``gaps`` pair would read as a passing audit.
115
190
  """
116
191
  from_prefix = from_prefix.strip() or None if from_prefix is not None else None
117
192
  if from_type is None and from_prefix is None:
118
193
  msg = "audit_reachability requires from_type or a non-empty from_prefix to select sources."
119
194
  raise ValueError(msg)
195
+ nodes = store.get_all_nodes()
196
+ # Select before traversing: an empty selection has nothing to prove.
197
+ sources = _select_sources(nodes, target_fqn, from_type, from_prefix)
198
+ if not sources:
199
+ raise _no_sources_error(nodes, target_fqn, from_type, from_prefix)
120
200
  engine = QueryEngine(store)
121
201
  # `is not None`, not `or`: an explicit empty frozenset (no-traversal, target-only)
122
202
  # is a valid intent and must not be overridden by the enforcement default.
@@ -127,8 +207,6 @@ def audit_reachability(
127
207
  reaching = {node.id for node in upstream_nodes}
128
208
  covered: list[NodeRef] = []
129
209
  gaps: list[NodeRef] = []
130
- for source in sorted(_select_sources(store, from_type, from_prefix), key=lambda n: n.id):
131
- if source.id == target_fqn:
132
- continue
210
+ for source in sorted(sources, key=lambda n: n.id):
133
211
  (covered if source.id in reaching else gaps).append(_ref(source))
134
212
  return ReachabilityAudit(target=target_fqn, covered=covered, gaps=gaps)
@@ -28,6 +28,10 @@ def graph_to_json(
28
28
  ``coverage`` is the traversal's local resolution coverage (#201). A
29
29
  structure query follows no calls and passes none, so the key is omitted
30
30
  rather than reported as a zero it did not measure.
31
+
32
+ Edges are one per distinct ``(src, type, dst)``, as in the Mermaid view: two
33
+ call sites to the same function are separate graph edges, but without a line
34
+ number the payload would state the same fact twice (#463).
31
35
  """
32
36
  payload: dict[str, Any] = {
33
37
  "root": root,
@@ -40,16 +44,26 @@ def graph_to_json(
40
44
  }
41
45
  for node in nodes
42
46
  ],
43
- "edges": [
44
- {
47
+ "edges": _distinct_edges(edges),
48
+ }
49
+ if coverage is not None:
50
+ payload["coverage"] = coverage.model_dump()
51
+ return payload
52
+
53
+
54
+ def _distinct_edges(edges: list[Edge]) -> list[dict[str, Any]]:
55
+ """Collapse edges sharing ``(src, type, dst)``, first-seen order, highest confidence kept."""
56
+ distinct: dict[tuple[str, str, str], dict[str, Any]] = {}
57
+ for edge in edges:
58
+ key = (edge.source, edge.type.value, edge.target)
59
+ seen = distinct.get(key)
60
+ if seen is None:
61
+ distinct[key] = {
45
62
  "src": edge.source,
46
63
  "dst": edge.target,
47
64
  "type": edge.type.value,
48
65
  "confidence": edge.confidence,
49
66
  }
50
- for edge in edges
51
- ],
52
- }
53
- if coverage is not None:
54
- payload["coverage"] = coverage.model_dump()
55
- return payload
67
+ elif edge.confidence > seen["confidence"]:
68
+ seen["confidence"] = edge.confidence
69
+ return list(distinct.values())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codegraph-brain
3
- Version: 0.23.0
3
+ Version: 0.23.2
4
4
  Summary: Semantic code graph for AI agents — deterministic FQN resolution, impact analysis and architectural drift gates, exposed over MCP.
5
5
  Project-URL: Homepage, https://github.com/zaebee/codegraph-brain
6
6
  Project-URL: Repository, https://github.com/zaebee/codegraph-brain
@@ -1,11 +1,11 @@
1
1
  cgis/__init__.py,sha256=3QdRGRHRQHrjQYXrPCo1uCZKcx5C-f098rpT21gq3RU,271
2
2
  cgis/__main__.py,sha256=w9qXbHyKaCVxkdm7i_NDSc-Mz07ivIoaAAVFGvs3Plo,326
3
- cgis/cli.py,sha256=cMEDjjpuaDdnvBGLmmG93olTGlQWfgdIALfyDyvZLDY,69945
3
+ cgis/cli.py,sha256=tLUfKUyEFG0LtU3xRrsBvyGOw0i08MKb7q1tJaCq1Ws,71133
4
4
  cgis/pipeline.py,sha256=c-z6MJb9H06hWZQfvVwGQL2PUN3k0-Am-PCAHfMK0nQ,17048
5
5
  cgis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  cgis/api/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  cgis/api/__init__.py,sha256=wvFZK6oZspFGmonBz2rDg1aXxK7DHuE87or_w_cwY28,91
8
- cgis/api/mcp_server.py,sha256=7p82YJK4orChNdJN3YOkWmLNB0T_01HzPcSkAt_F3fk,44422
8
+ cgis/api/mcp_server.py,sha256=M2lq_Eo8UO0H47yTrbyBvSPzsp8sKH01jQ4B2HWH1dg,44602
9
9
  cgis/core/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  cgis/core/coverage.py,sha256=AmeZ_mij5Q382MAqtUmDRmJ_lHLPsV73k0_zksaWc6c,2469
11
11
  cgis/core/freshness.py,sha256=s1ahMqizX5jRo5Sj6T-2YfNq1d-jsRqisfne5m4yagw,1395
@@ -60,7 +60,7 @@ cgis/query/analysis/cohesion.py,sha256=o_xXGbFqD0RgRSHxbsIBD-B3Spmtxv-RCy1MbB_IG
60
60
  cgis/query/analysis/health.py,sha256=3eZokxeH_be7l7MNuLoLpRIZZ5B3N0DoA1TKMDAkjBs,4989
61
61
  cgis/query/analysis/suggest_service.py,sha256=K_eJsfP54QGRSI3rBFB1Y94G-asP30EBZoVUlr1wkm8,10059
62
62
  cgis/query/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
- cgis/query/context/audit.py,sha256=yKFc0EgzH4OFm_ZUZ1yeMAB93jCcGZmqfXrCuGup6ec,6157
63
+ cgis/query/context/audit.py,sha256=U8ZPvFxTFwsXKTbZzXaE9GKEZ6RSD_xCGFciF1sMqHk,9566
64
64
  cgis/query/context/context_service.py,sha256=V0YtJaImgs7cwihejaBzKaOWa8Iax1T2OGrC-W6HbWs,5849
65
65
  cgis/query/context/orphans.py,sha256=-28Jc7CqXXQIlH81LGlQihLtlIQLO0UK20DuEn3ZAuI,8064
66
66
  cgis/query/context/prompt.py,sha256=H38K0MN_PBaBdK-1nmN1873rOwADVvVYmcxKECLQkLU,8222
@@ -75,7 +75,7 @@ cgis/query/drift/ontology_init.py,sha256=cWcr_UjUJRV9w8MGZITt4m6kvQ3asbKLZaN402E
75
75
  cgis/query/drift/quotient.py,sha256=4tV29yk07VYfbrAo7PIrWQ4BagZ0T6nItWP_8Macx_4,2711
76
76
  cgis/query/drift/triads.py,sha256=OoGxSAsU9QzHP2MboYkXJ3Jg7PyoE9tO6BfX65fwyYQ,6150
77
77
  cgis/query/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
- cgis/query/render/graph_json.py,sha256=wJ-bXxEC9S9fCWFRkRM4QOwEJ3OUTHYFNJ-OX2uxdU4,1899
78
+ cgis/query/render/graph_json.py,sha256=Wxu0_4oxa7o6IKvjTnJbPgV-Hlcl211DDi_UwFjABH0,2622
79
79
  cgis/query/render/mermaid.py,sha256=OfvAIdqcqfCS8vvDdWTovGe9dYbLu_DEhRmjmDv8bEc,10749
80
80
  cgis/query/render/metrics.py,sha256=23iEdj29_Cry5VlLvDHgLM8f3_CewARgxllkPgEg3nI,18179
81
81
  cgis/resolver/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -87,8 +87,8 @@ cgis/resolver/symbols.py,sha256=A88dOb7ciEfWaS4KCK0Ho4cBC2IZisNrBfYp-f1LFYs,1254
87
87
  cgis/resolver/uplift.py,sha256=Hkye1q-kI-gKO6GIBYv3gUZj1AC0-_mkpoIpmwVsG4g,9591
88
88
  cgis/storage/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
89
  cgis/storage/sqlite_store.py,sha256=tlkg9kbiAFm0rth7ndsRPyv3cjDCj3dpvs7Pp_FTBno,45671
90
- codegraph_brain-0.23.0.dist-info/METADATA,sha256=eSkggudqSecXPbucmWHFtUwQrXvawtLanC9aBW2BQC0,11771
91
- codegraph_brain-0.23.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
92
- codegraph_brain-0.23.0.dist-info/entry_points.txt,sha256=mWajEQlAoWuiLyCQdD1TvvguloKQFmzKb52-g9PB_R4,123
93
- codegraph_brain-0.23.0.dist-info/licenses/LICENSE,sha256=1X36nOG5VocDaEGzAZqIv7uawy_7sWlFoiwECwx5aBI,1065
94
- codegraph_brain-0.23.0.dist-info/RECORD,,
90
+ codegraph_brain-0.23.2.dist-info/METADATA,sha256=Ul_KuCONXckwgxLZ6rE6LxtefLk-IQVyDmsJVKGhw9E,11771
91
+ codegraph_brain-0.23.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
92
+ codegraph_brain-0.23.2.dist-info/entry_points.txt,sha256=mWajEQlAoWuiLyCQdD1TvvguloKQFmzKb52-g9PB_R4,123
93
+ codegraph_brain-0.23.2.dist-info/licenses/LICENSE,sha256=1X36nOG5VocDaEGzAZqIv7uawy_7sWlFoiwECwx5aBI,1065
94
+ codegraph_brain-0.23.2.dist-info/RECORD,,