codegraph-brain 0.23.2__py3-none-any.whl → 0.23.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.
cgis/api/mcp_server.py CHANGED
@@ -17,7 +17,7 @@ from pydantic import Field
17
17
  from cgis import __version__
18
18
  from cgis.core.coverage import TraversalCoverage
19
19
  from cgis.core.freshness import Freshness, FreshnessState
20
- from cgis.core.models import Edge, Node, NodeType
20
+ from cgis.core.models import Edge, EdgeType, Node, NodeType
21
21
  from cgis.extractors.python_extractor import PythonExtractor
22
22
  from cgis.extractors.typescript_extractor import TypeScriptExtractor
23
23
  from cgis.pipeline import IngestionPipeline
@@ -28,7 +28,7 @@ from cgis.query.context.orphans import find_orphan_classes
28
28
  from cgis.query.drift.drift_service import analyze_drift
29
29
  from cgis.query.drift.fractal import analyze_fractal_db
30
30
  from cgis.query.drift.ontology_init import propose_ontology
31
- from cgis.query.engine import QueryEngine
31
+ from cgis.query.engine import BEHAVIORAL_EDGE_TYPES, QueryEngine
32
32
  from cgis.query.fqn import resolve_fqn
33
33
  from cgis.query.render.graph_json import graph_to_json
34
34
  from cgis.query.render.mermaid import MermaidCompiler
@@ -65,6 +65,31 @@ Fqn = Annotated[
65
65
  "to look a name up."
66
66
  ),
67
67
  ]
68
+ IncludeStructure = Annotated[
69
+ bool,
70
+ Field(
71
+ description="Also follow containment (CONTAINS/DECLARES): a module's or class's own "
72
+ "members, and the class or file enclosing a symbol. Off by default, as in the CLI; "
73
+ "cgis_get_structure is the tool for members alone."
74
+ ),
75
+ ]
76
+ IncludeExternal = Annotated[
77
+ bool,
78
+ Field(
79
+ description="Also return stdlib, third-party and unresolved call targets — calls on "
80
+ "values whose type is decided at runtime. Off by default, as in the CLI, because they "
81
+ "dominate the payload; in json, coverage/top_unresolved still counts what was dropped."
82
+ ),
83
+ ]
84
+
85
+
86
+ def _traversal_filters(
87
+ include_structure: bool, include_external: bool
88
+ ) -> tuple[frozenset[EdgeType] | None, bool]:
89
+ """Edge-type allowlist and external flag for trace/impact — the CLI's defaults (#481)."""
90
+ return (None if include_structure else BEHAVIORAL_EDGE_TYPES), include_external
91
+
92
+
68
93
  OutputFormat = Annotated[
69
94
  str,
70
95
  Field(
@@ -363,14 +388,26 @@ def cgis_trace_flow(
363
388
  depth: Annotated[
364
389
  int,
365
390
  Field(
366
- description="Maximum edge hops downstream. Every edge type counts as a hop — "
367
- "CALLS, but also IMPORTS, CONTAINS and REFERENCES — so from a module the first "
368
- "hops are mostly imports and structure."
391
+ description="Maximum edge hops downstream, over calls, imports, inheritance, DI "
392
+ "dependencies and references (plus containment with include_structure)."
369
393
  ),
370
394
  ] = 3,
371
395
  output_format: OutputFormat = "mermaid",
396
+ include_structure: IncludeStructure = False,
397
+ include_external: IncludeExternal = False,
372
398
  ) -> str:
373
- """Trace the execution call-graph starting from a specific FQN downwards.
399
+ """Downstream subgraph of one FQN: everything it reaches within ``depth`` hops.
400
+
401
+ Follows every edge except containment — in practice calls, imports,
402
+ inheritance, DI dependencies and references — between internal code, so this
403
+ answers "what does X depend on?". Containment and
404
+ stdlib/third-party nodes are left out unless ``include_structure`` /
405
+ ``include_external`` ask for them (external covers stdlib, third-party and
406
+ unresolved call targets) — the same view as the CLI's ``trace``.
407
+ For what depends on X use
408
+ ``cgis_analyze_impact``; for only the members of a module or class,
409
+ ``cgis_get_structure``; for a source-included brief to read before editing
410
+ one symbol, ``cgis_context``.
374
411
 
375
412
  ``output_format="mermaid"`` (default) returns a human-readable diagram;
376
413
  ``"json"`` returns a joinable ``{root, nodes, edges, coverage}`` payload
@@ -390,8 +427,13 @@ def cgis_trace_flow(
390
427
  res = resolve_fqn(store, fqn)
391
428
  if res.resolved is None:
392
429
  return _resolution_error(fqn, res.candidates, res.truncated)
430
+ allowed, show_external = _traversal_filters(include_structure, include_external)
393
431
  result = QueryEngine(store).get_flow_result(
394
- res.resolved, max_depth=depth, with_coverage=_wants_json(output_format)
432
+ res.resolved,
433
+ max_depth=depth,
434
+ allowed_edge_types=allowed,
435
+ show_external=show_external,
436
+ with_coverage=_wants_json(output_format),
395
437
  )
396
438
  except Exception as exc:
397
439
  return f"❌ {exc}"
@@ -416,16 +458,28 @@ def cgis_analyze_impact(
416
458
  depth: Annotated[
417
459
  int,
418
460
  Field(
419
- description="Maximum edge hops upstream. Every edge type counts as a hop — "
420
- "CALLS, but also IMPORTS, CONTAINS and REFERENCES — so modules importing the "
421
- "target and the file containing it appear alongside its callers."
461
+ description="Maximum edge hops upstream, over callers, importers, subclasses, "
462
+ "type references and DI dependents (plus the enclosing class or file with "
463
+ "include_structure)."
422
464
  ),
423
465
  ] = 3,
424
466
  output_format: OutputFormat = "mermaid",
467
+ include_structure: IncludeStructure = False,
468
+ include_external: IncludeExternal = False,
425
469
  ) -> str:
426
- """Analyse transitive upstream callers of a specific FQN.
427
-
428
- Answers "what breaks if I change X?". ``output_format="mermaid"`` (default)
470
+ """Upstream subgraph of one FQN: everything that reaches it within ``depth`` hops.
471
+
472
+ Follows every edge except containment — in practice callers, importers,
473
+ subclasses, type references and DI dependents — within internal code, so this
474
+ answers "what breaks if I change X?". The
475
+ enclosing class or file and stdlib/third-party nodes are left out unless
476
+ ``include_structure`` / ``include_external`` ask for them — the same view as
477
+ the CLI's ``impact``. For what X depends on use
478
+ ``cgis_trace_flow``; for only the members of a module or class,
479
+ ``cgis_get_structure``; for a source-included brief to read before editing
480
+ one symbol, ``cgis_context``.
481
+
482
+ ``output_format="mermaid"`` (default)
429
483
  returns a diagram; ``"json"`` returns a joinable ``{root, nodes, edges,
430
484
  coverage}`` payload with real FQNs — letting an agent compute set
431
485
  differences (e.g. "which route handlers never reach ``verify_ownership``?")
@@ -443,8 +497,13 @@ def cgis_analyze_impact(
443
497
  res = resolve_fqn(store, fqn)
444
498
  if res.resolved is None:
445
499
  return _resolution_error(fqn, res.candidates, res.truncated)
500
+ allowed, show_external = _traversal_filters(include_structure, include_external)
446
501
  result = QueryEngine(store).get_impact_result(
447
- res.resolved, max_depth=depth, with_coverage=_wants_json(output_format)
502
+ res.resolved,
503
+ max_depth=depth,
504
+ allowed_edge_types=allowed,
505
+ show_external=show_external,
506
+ with_coverage=_wants_json(output_format),
448
507
  )
449
508
  except Exception as exc:
450
509
  return f"❌ {exc}"
@@ -471,10 +530,13 @@ def cgis_get_structure(
471
530
  ] = 2,
472
531
  output_format: OutputFormat = "mermaid",
473
532
  ) -> str:
474
- """Show the structural layout (CONTAINS/DECLARES) of a module or class.
533
+ """Members of a module or class: the classes, functions and methods it contains.
534
+
535
+ Follows containment (CONTAINS/DECLARES) only, so no call or import appears.
536
+ For how the code connects use ``cgis_trace_flow`` (what it depends on) or
537
+ ``cgis_analyze_impact`` (what depends on it).
475
538
 
476
- Traverses only containment edges — no call-graph noise — matching the CLI
477
- ``structure`` command. ``output_format="mermaid"`` (default) returns a
539
+ Matches the CLI ``structure`` command. ``output_format="mermaid"`` (default) returns a
478
540
  diagram of the hierarchy rooted at the given FQN; ``"json"`` returns the
479
541
  joinable ``{root, nodes, edges}`` payload with real FQNs.
480
542
  """
@@ -785,7 +847,15 @@ def cgis_context(
785
847
  ),
786
848
  ] = "",
787
849
  ) -> str:
788
- """Compile an agent-facing GraphRAG context package for a focal FQN (#19).
850
+ """Prompt-ready brief on one FQN: its source, class, direct callers and callees.
851
+
852
+ Call this before editing a symbol, instead of reading its files. It follows
853
+ calls only, one hop by default. Source is included when the file is found
854
+ (see ``source_root``), and the domain when the graph was tagged with one.
855
+ For a multi-hop subgraph
856
+ over calls, imports, inheritance and references without source, use
857
+ ``cgis_trace_flow`` (downstream) or
858
+ ``cgis_analyze_impact`` (upstream).
789
859
 
790
860
  Returns an XML-tagged prompt — the focal node's source, its enclosing class,
791
861
  its architectural domain boundary, direct callers (upstream ripple) and
@@ -836,7 +906,7 @@ def cgis_metrics(
836
906
  ),
837
907
  ] = None,
838
908
  ) -> str:
839
- """Whole-graph architectural metrics — coupling bottlenecks + God classes (#16).
909
+ """Whole-graph architectural metrics — coupling bottlenecks, God classes, PageRank.
840
910
 
841
911
  Returns JSON ``{bottlenecks, god_classes, critical}`` computed with vectorized
842
912
  DuckDB aggregations over the whole graph (fan-in/fan-out coupling,
@@ -894,7 +964,7 @@ def cgis_find_orphans(
894
964
  bool, Field(description="Include machine-generated classes, which are hidden by default.")
895
965
  ] = False,
896
966
  ) -> str:
897
- """Classes nothing in production builds, extends or names — dead-code candidates (#415).
967
+ """Classes nothing in production builds, extends or names — dead-code candidates.
898
968
 
899
969
  Finds classes that no test, type checker or linter flags, because each is
900
970
  still imported somewhere: a package re-export keeps a class importable long
@@ -976,7 +1046,7 @@ def cgis_audit_reachability(
976
1046
  Field(description="Maximum reachability depth; a longer path is reported as a gap."),
977
1047
  ] = 5,
978
1048
  ) -> str:
979
- """Reachability/authorization audit — which sources never reach a checkpoint (#172).
1049
+ """Reachability/authorization audit — which sources never reach a checkpoint.
980
1050
 
981
1051
  The headline use is **IDOR/authz coverage**: list every route handler that does
982
1052
  NOT transitively reach an ownership check. Reachability follows behavioral edges
@@ -76,7 +76,10 @@ def _identifiers_outside_imports(root: BaseNode, code_bytes: bytes) -> set[str]:
76
76
  # strings (parent `type`): scanning every string would let a docstring
77
77
  # mentioning a name mask a real re-export.
78
78
  text = code_bytes[node.start_byte : node.end_byte].decode("utf8")
79
- used.update(re.findall(r"[A-Za-z_][A-Za-z0-9_]*", text))
79
+ # `\w`, so a non-ASCII identifier in a quoted annotation counts as a
80
+ # use. Matching more here can only mark an import used, which is the
81
+ # safe direction for the unused-import sweep this feeds.
82
+ used.update(re.findall(r"[^\W\d]\w*", text))
80
83
  stack.extend(node.children)
81
84
  return used
82
85
 
@@ -197,6 +200,47 @@ def _collect_local_type(
197
200
  )
198
201
 
199
202
 
203
+ def _walk_one_decorator(
204
+ functions: FunctionHandler,
205
+ decorator: BaseNode,
206
+ code_bytes: bytes,
207
+ file_path: str,
208
+ edges: list[Edge],
209
+ owner_fqn: str,
210
+ name_refs_acc: list[tuple[str, str, int]] | None,
211
+ ) -> None:
212
+ """Record one decorator's calls and names, skipping its own head (#429).
213
+
214
+ Everything except the decorator's own head name, which
215
+ `process_function_node` already records as `raw_call:<name>` from its
216
+ decorator list — walking that too emitted `router.post` twice under two
217
+ ids, and its name a third time as a REFERENCES.
218
+
219
+ Skipping the *head* rather than "everything but the outermost call's
220
+ arguments" is what keeps the rarer shapes: `@a.b(X)(Y)`, `@registry[X]`
221
+ and `@(deco(X))` all name something the narrower rule walked straight past.
222
+ """
223
+ head = _decorator_head(decorator)
224
+ stack = [c for c in decorator.children if c.type not in ("comment", "@")]
225
+ while stack:
226
+ current = stack.pop()
227
+ if head is not None and current.id == head.id:
228
+ continue
229
+ if current.type == "call" and _calls_directly(current, head):
230
+ # `@router.post(...)` — the decorator list already recorded this
231
+ # exact call as `raw_call:router.post`. Its arguments still need
232
+ # walking, so recurse without processing it.
233
+ stack.extend(current.children)
234
+ continue
235
+ if current.type == "call":
236
+ functions.process_call_node(
237
+ current, code_bytes, file_path, owner_fqn, edges, emit_di=False
238
+ )
239
+ elif current.type == "identifier" and name_refs_acc is not None and is_name_load(current):
240
+ _append_name_candidates(current, code_bytes, owner_fqn, name_refs_acc)
241
+ stack.extend(current.children)
242
+
243
+
200
244
  class PythonExtractor(BaseExtractor):
201
245
  """
202
246
  A concrete extractor for Python source code using tree-sitter.
@@ -589,38 +633,9 @@ class PythonExtractor(BaseExtractor):
589
633
  else `_walk` would have handled here.
590
634
  """
591
635
  for decorator in (c for c in node.children if c.type == "decorator"):
592
- # Everything except the decorator's own head name, which
593
- # `process_function_node` already records as `raw_call:<name>` from
594
- # its decorator list — walking that too emitted `router.post` twice
595
- # under two ids, and its name a third time as a REFERENCES.
596
- #
597
- # Skipping the *head* rather than "everything but the outermost
598
- # call's arguments" is what keeps the rarer shapes: `@a.b(X)(Y)`,
599
- # `@registry[X]` and `@(deco(X))` all name something the narrower
600
- # rule walked straight past.
601
- head = _decorator_head(decorator)
602
- stack = [c for c in decorator.children if c.type not in ("comment", "@")]
603
- while stack:
604
- current = stack.pop()
605
- if head is not None and current.id == head.id:
606
- continue
607
- if current.type == "call" and _calls_directly(current, head):
608
- # `@router.post(...)` — the decorator list already recorded
609
- # this exact call as `raw_call:router.post`. Its arguments
610
- # still need walking, so recurse without processing it.
611
- stack.extend(current.children)
612
- continue
613
- if current.type == "call":
614
- self._functions.process_call_node(
615
- current, code_bytes, file_path, owner_fqn, edges, emit_di=False
616
- )
617
- elif (
618
- current.type == "identifier"
619
- and name_refs_acc is not None
620
- and is_name_load(current)
621
- ):
622
- _append_name_candidates(current, code_bytes, owner_fqn, name_refs_acc)
623
- stack.extend(current.children)
636
+ _walk_one_decorator(
637
+ self._functions, decorator, code_bytes, file_path, edges, owner_fqn, name_refs_acc
638
+ )
624
639
 
625
640
  def _handle_decorated_definition(
626
641
  self,
@@ -779,14 +779,25 @@ class SQLiteStore:
779
779
  reason=f"ingest root {base} no longer exists — {hint}",
780
780
  )
781
781
 
782
- tracked = self.get_tracked_source_files()
782
+ changed, missing = self._count_stale(base, self.get_tracked_source_files(), ingested_at)
783
+ if changed or missing:
784
+ return Freshness(state=FreshnessState.STALE, changed=changed, missing=missing)
785
+ return Freshness(state=FreshnessState.FRESH)
786
+
787
+ def _count_stale(self, base: str, tracked: set[str], ingested_at: float) -> tuple[int, int]:
788
+ """Files newer than the ingest, and files the graph has that the tree no longer does.
789
+
790
+ The two halves are complementary: a file's own mtime catches an edit, and
791
+ its directory's mtime catches an addition, a deletion or a new subdirectory.
792
+
793
+ `os.stat` on joined strings, not pathlib — the PTH rules are suppressed
794
+ deliberately here. Measured on this repository's own graph: the same 811
795
+ files cost 3.7 ms this way and 18.8 ms through `Path.stat()`, which is
796
+ slower than the tree walk this design exists to avoid. Tidying these to
797
+ `Path` would triple a cost paid on every query.
798
+ """
783
799
  changed = 0
784
800
  missing = 0
785
- # `os.stat` on joined strings, not pathlib — the PTH rules are suppressed
786
- # deliberately here. Measured on this repository's own graph: the same 811
787
- # files cost 3.7 ms this way and 18.8 ms through `Path.stat()`, which is
788
- # slower than the tree walk this design exists to avoid. Tidying these to
789
- # `Path` would triple a cost paid on every query.
790
801
  for rel in tracked:
791
802
  try:
792
803
  if os.stat(os.path.join(base, rel)).st_mtime > ingested_at: # noqa: PTH116,PTH118
@@ -803,10 +814,7 @@ class SQLiteStore:
803
814
  # Nothing added: the report says "N missing" of *files*, and every
804
815
  # file under a vanished directory has already been counted above.
805
816
  continue
806
-
807
- if changed or missing:
808
- return Freshness(state=FreshnessState.STALE, changed=changed, missing=missing)
809
- return Freshness(state=FreshnessState.FRESH)
817
+ return changed, missing
810
818
 
811
819
  def _directory_gained_a_source(
812
820
  self, path: str, tracked: set[str], base: str, ingested_at: float
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codegraph-brain
3
- Version: 0.23.2
3
+ Version: 0.23.4
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
@@ -5,7 +5,7 @@ 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=M2lq_Eo8UO0H47yTrbyBvSPzsp8sKH01jQ4B2HWH1dg,44602
8
+ cgis/api/mcp_server.py,sha256=b3HiTphKD1UcmKP1XwsawTrRLkvAAPRZLACxy1z1X8Y,47917
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
@@ -22,7 +22,7 @@ cgis/extractors/_python_imports.py,sha256=qvrO6sUG0O9O4k_P0TzibEu0dPwciW_A25CREy
22
22
  cgis/extractors/_python_types.py,sha256=N6xIOxVikpwkwrPfv1s_MwX6rw8QwzW2VeBXi7-g328,3462
23
23
  cgis/extractors/_typescript_bindings.py,sha256=_G7rN30LVMTeNBuEHY9LeLOM6PZV3J_2qYp42PRNDcg,6099
24
24
  cgis/extractors/base.py,sha256=4lhdFRqsj-3vBpeHZjnE3f9IWZ_WhbKQBxVAMuhzTho,1502
25
- cgis/extractors/python_extractor.py,sha256=KLp9bq4Bcqa6_upZdFGjTW8S-wOpHTGJMkNoAR3lcs0,29533
25
+ cgis/extractors/python_extractor.py,sha256=U6MegkLa8rt33DGiv_4rNgNWYEocTqbv5hQ1lctOCb0,29884
26
26
  cgis/extractors/registry.py,sha256=SUZmkx34gfrCUa1cT09lWrT_Ngk1OK4fTMm4XJhwFWM,4078
27
27
  cgis/extractors/typescript_extractor.py,sha256=fs8l-KsyY35KnnsbWp9c5D_xeWdWRaCSLR_3N_S2ORA,11879
28
28
  cgis/guardian/__init__.py,sha256=H6BlKTayCLP34uGRC1zVt9NhN7RGo3tN9oep9Ja5SK0,48
@@ -86,9 +86,9 @@ cgis/resolver/js_builtins.py,sha256=cZ0F6DyqmPWVGHMgh0PVAv1VqGHO3IHs65cK9sX57CQ,
86
86
  cgis/resolver/symbols.py,sha256=A88dOb7ciEfWaS4KCK0Ho4cBC2IZisNrBfYp-f1LFYs,12545
87
87
  cgis/resolver/uplift.py,sha256=Hkye1q-kI-gKO6GIBYv3gUZj1AC0-_mkpoIpmwVsG4g,9591
88
88
  cgis/storage/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
- cgis/storage/sqlite_store.py,sha256=tlkg9kbiAFm0rth7ndsRPyv3cjDCj3dpvs7Pp_FTBno,45671
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,,
89
+ cgis/storage/sqlite_store.py,sha256=AXPlL1RGs38DmpIXuzrBY3JOavpSJDbwP5jorwAlD1c,46112
90
+ codegraph_brain-0.23.4.dist-info/METADATA,sha256=kyI_tand59eW6i0ggOa3KEBMDiagLy07Z5LtdntTjn4,11771
91
+ codegraph_brain-0.23.4.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
92
+ codegraph_brain-0.23.4.dist-info/entry_points.txt,sha256=mWajEQlAoWuiLyCQdD1TvvguloKQFmzKb52-g9PB_R4,123
93
+ codegraph_brain-0.23.4.dist-info/licenses/LICENSE,sha256=1X36nOG5VocDaEGzAZqIv7uawy_7sWlFoiwECwx5aBI,1065
94
+ codegraph_brain-0.23.4.dist-info/RECORD,,