codegraph-brain 0.7.0__py3-none-any.whl → 0.7.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/cli.py +116 -53
- cgis/query/drift/ontology_init.py +65 -0
- {codegraph_brain-0.7.0.dist-info → codegraph_brain-0.7.2.dist-info}/METADATA +16 -2
- {codegraph_brain-0.7.0.dist-info → codegraph_brain-0.7.2.dist-info}/RECORD +7 -7
- {codegraph_brain-0.7.0.dist-info → codegraph_brain-0.7.2.dist-info}/WHEEL +0 -0
- {codegraph_brain-0.7.0.dist-info → codegraph_brain-0.7.2.dist-info}/entry_points.txt +0 -0
- {codegraph_brain-0.7.0.dist-info → codegraph_brain-0.7.2.dist-info}/licenses/LICENSE +0 -0
cgis/cli.py
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
""" "CLI to run pipeline.
|
|
1
|
+
""" "CLI to run pipeline.
|
|
2
|
+
|
|
3
|
+
Rich markup convention (#148)
|
|
4
|
+
-----------------------------
|
|
5
|
+
Rich parses ``[...]`` in everything it renders, so any *data* reaching a console
|
|
6
|
+
sink must be wrapped in ``rich.markup.escape``. This applies to ``console.print``
|
|
7
|
+
f-strings, ``Table.add_row`` cells, and panel/tree labels alike.
|
|
8
|
+
|
|
9
|
+
The failure mode is silent: unescaped text is not styled, it *vanishes*. PR #144
|
|
10
|
+
shipped a line where ``[cgis-project]`` rendered as an empty string, so the
|
|
11
|
+
operator saw a blank where their domain name should have been.
|
|
12
|
+
|
|
13
|
+
wrong: console.print(f"[bold red]Not found:[/bold red] {path}")
|
|
14
|
+
right: console.print(f"[bold red]Not found:[/bold red] {escape(path)}")
|
|
15
|
+
|
|
16
|
+
Escape anything derived from config, the graph, the filesystem, CLI arguments, or
|
|
17
|
+
an exception message — names, FQNs, file paths, pattern names, ``{e}``. Literal
|
|
18
|
+
style tags you wrote yourself and pure numbers need no escaping.
|
|
19
|
+
"""
|
|
2
20
|
|
|
3
21
|
import dataclasses
|
|
4
22
|
import json as _json
|
|
@@ -190,10 +208,12 @@ def ingest(
|
|
|
190
208
|
pipeline = IngestionPipeline(extractors, domains_config=domains)
|
|
191
209
|
|
|
192
210
|
if domains and not Path(domains).is_file():
|
|
193
|
-
console.print(
|
|
211
|
+
console.print(
|
|
212
|
+
f"[bold red]❌ Domains config file not found:[/bold red] {escape(str(domains))}"
|
|
213
|
+
)
|
|
194
214
|
raise typer.Exit(code=1)
|
|
195
215
|
|
|
196
|
-
console.print(f"[bold blue]🚀 Starting ingestion for:[/bold blue] {path}")
|
|
216
|
+
console.print(f"[bold blue]🚀 Starting ingestion for:[/bold blue] {escape(path)}")
|
|
197
217
|
|
|
198
218
|
if incremental and output.endswith(".json"):
|
|
199
219
|
console.print(
|
|
@@ -233,7 +253,7 @@ def ingest(
|
|
|
233
253
|
console.print("[bold green]✅ Success![/bold green] Graph data ready.")
|
|
234
254
|
|
|
235
255
|
except Exception as e:
|
|
236
|
-
console.print(f"[bold red]❌ Error during ingestion:[/bold red] {e}")
|
|
256
|
+
console.print(f"[bold red]❌ Error during ingestion:[/bold red] {escape(str(e))}")
|
|
237
257
|
raise typer.Exit(code=1) from e
|
|
238
258
|
|
|
239
259
|
|
|
@@ -272,16 +292,16 @@ def _resolve_cli_fqn(store: SQLiteStore, target: str, kind: str) -> str:
|
|
|
272
292
|
resolution = resolve_fqn(store, target)
|
|
273
293
|
if resolution.resolved is None:
|
|
274
294
|
if resolution.candidates:
|
|
275
|
-
console.print(f"[bold red]❌ Ambiguous FQN:[/bold red] {target}")
|
|
295
|
+
console.print(f"[bold red]❌ Ambiguous FQN:[/bold red] {escape(target)}")
|
|
276
296
|
for candidate in resolution.candidates:
|
|
277
|
-
console.print(f" [dim]- {candidate}[/dim]")
|
|
297
|
+
console.print(f" [dim]- {escape(candidate)}[/dim]")
|
|
278
298
|
if resolution.truncated:
|
|
279
299
|
console.print(_TRUNCATED_HINT)
|
|
280
300
|
else:
|
|
281
|
-
console.print(f"[bold red]❌ {kind} not found in graph:[/bold red] {target}")
|
|
301
|
+
console.print(f"[bold red]❌ {kind} not found in graph:[/bold red] {escape(target)}")
|
|
282
302
|
raise typer.Exit(code=1)
|
|
283
303
|
if resolution.via_suffix:
|
|
284
|
-
console.print(f"[dim]Resolved '{target}' → '{resolution.resolved}'[/dim]")
|
|
304
|
+
console.print(f"[dim]Resolved '{escape(target)}' → '{escape(resolution.resolved)}'[/dim]")
|
|
285
305
|
return resolution.resolved
|
|
286
306
|
|
|
287
307
|
|
|
@@ -368,7 +388,9 @@ def trace(
|
|
|
368
388
|
"""
|
|
369
389
|
path = Path(db)
|
|
370
390
|
if not path.is_file():
|
|
371
|
-
console.print(
|
|
391
|
+
console.print(
|
|
392
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
393
|
+
)
|
|
372
394
|
raise typer.Exit(code=1)
|
|
373
395
|
|
|
374
396
|
allowed: frozenset[EdgeType] | None = None if show_structure else BEHAVIORAL_EDGE_TYPES
|
|
@@ -497,7 +519,9 @@ def impact(
|
|
|
497
519
|
"""
|
|
498
520
|
path = Path(db)
|
|
499
521
|
if not path.is_file():
|
|
500
|
-
console.print(
|
|
522
|
+
console.print(
|
|
523
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
524
|
+
)
|
|
501
525
|
raise typer.Exit(code=1)
|
|
502
526
|
|
|
503
527
|
allowed: frozenset[EdgeType] | None = None if show_structure else BEHAVIORAL_EDGE_TYPES
|
|
@@ -562,14 +586,16 @@ def validate(
|
|
|
562
586
|
"""
|
|
563
587
|
path = Path(db)
|
|
564
588
|
if not path.is_file():
|
|
565
|
-
console.print(
|
|
589
|
+
console.print(
|
|
590
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
591
|
+
)
|
|
566
592
|
raise typer.Exit(code=1)
|
|
567
593
|
|
|
568
594
|
try:
|
|
569
595
|
with SQLiteStore(db) as store:
|
|
570
596
|
stats = store.get_edge_stats()
|
|
571
597
|
except Exception as e:
|
|
572
|
-
console.print(f"[bold red]❌ Error reading database:[/bold red] {e}")
|
|
598
|
+
console.print(f"[bold red]❌ Error reading database:[/bold red] {escape(str(e))}")
|
|
573
599
|
raise typer.Exit(code=1) from e
|
|
574
600
|
|
|
575
601
|
def _pct(n: int) -> str:
|
|
@@ -600,7 +626,7 @@ def validate(
|
|
|
600
626
|
top_table.add_column("count", justify="right", style="yellow")
|
|
601
627
|
for target, count in stats.top_unresolved:
|
|
602
628
|
name = target.removeprefix(RAW_CALL_PREFIX)
|
|
603
|
-
top_table.add_row(name, str(count))
|
|
629
|
+
top_table.add_row(escape(name), str(count))
|
|
604
630
|
console.print(top_table)
|
|
605
631
|
|
|
606
632
|
threshold_pct = threshold * 100
|
|
@@ -637,7 +663,9 @@ def find(
|
|
|
637
663
|
"""
|
|
638
664
|
path = Path(db)
|
|
639
665
|
if not path.is_file():
|
|
640
|
-
console.print(
|
|
666
|
+
console.print(
|
|
667
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
668
|
+
)
|
|
641
669
|
raise typer.Exit(code=1)
|
|
642
670
|
|
|
643
671
|
kinds = (kind.strip().upper(),) if kind and kind.strip() else ()
|
|
@@ -654,7 +682,12 @@ def find(
|
|
|
654
682
|
table.add_column("FQN", style="yellow")
|
|
655
683
|
table.add_column("Location", style="dim")
|
|
656
684
|
for node in matches:
|
|
657
|
-
table.add_row(
|
|
685
|
+
table.add_row(
|
|
686
|
+
escape(node.name),
|
|
687
|
+
node.type.value,
|
|
688
|
+
escape(node.id),
|
|
689
|
+
escape(f"{node.file_path}:{node.start_line}"),
|
|
690
|
+
)
|
|
658
691
|
console.print(table)
|
|
659
692
|
|
|
660
693
|
|
|
@@ -675,14 +708,16 @@ def structure(
|
|
|
675
708
|
"""
|
|
676
709
|
path = Path(db)
|
|
677
710
|
if not path.is_file():
|
|
678
|
-
console.print(
|
|
711
|
+
console.print(
|
|
712
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
713
|
+
)
|
|
679
714
|
raise typer.Exit(code=1)
|
|
680
715
|
|
|
681
716
|
# Normalize file path → FQN
|
|
682
717
|
if "/" in target or "\\" in target or target.endswith(".py"):
|
|
683
718
|
normalized = target.replace("\\", "/").removeprefix("./")
|
|
684
719
|
target = file_path_to_module_fqn(normalized)
|
|
685
|
-
console.print(f"[dim]→ FQN: {target}[/dim]")
|
|
720
|
+
console.print(f"[dim]→ FQN: {escape(target)}[/dim]")
|
|
686
721
|
|
|
687
722
|
with SQLiteStore(db) as store:
|
|
688
723
|
target = _resolve_cli_fqn(store, target, "Node")
|
|
@@ -696,7 +731,7 @@ def structure(
|
|
|
696
731
|
typer.echo(_render_graph(output_format, target, nodes, edges))
|
|
697
732
|
return
|
|
698
733
|
|
|
699
|
-
console.print(f"[bold blue]📦 Structure of:[/bold blue] {target}\n")
|
|
734
|
+
console.print(f"[bold blue]📦 Structure of:[/bold blue] {escape(target)}\n")
|
|
700
735
|
root_label = (
|
|
701
736
|
f"[bold cyan]{target_node.type.value}[/bold cyan] [yellow]{target_node.id}[/yellow]"
|
|
702
737
|
)
|
|
@@ -765,7 +800,9 @@ def analyze(
|
|
|
765
800
|
"""
|
|
766
801
|
path = Path(db)
|
|
767
802
|
if not path.is_file():
|
|
768
|
-
console.print(
|
|
803
|
+
console.print(
|
|
804
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
805
|
+
)
|
|
769
806
|
raise typer.Exit(code=1)
|
|
770
807
|
|
|
771
808
|
with SQLiteStore(db) as store:
|
|
@@ -809,8 +846,8 @@ def analyze(
|
|
|
809
846
|
f"severity=[bold {colour}]{a.severity_score:.2f}[/bold {colour}]"
|
|
810
847
|
)
|
|
811
848
|
for key, val in a.metrics.items():
|
|
812
|
-
console.print(f" [dim]{key}:[/dim] {val}")
|
|
813
|
-
console.print(f" [italic]💡 {a.refactoring_hint}[/italic]\n")
|
|
849
|
+
console.print(f" [dim]{escape(str(key))}:[/dim] {escape(str(val))}")
|
|
850
|
+
console.print(f" [italic]💡 {escape(a.refactoring_hint)}[/italic]\n")
|
|
814
851
|
|
|
815
852
|
|
|
816
853
|
_DEFAULT_METRICS = "guardian_metrics.jsonl"
|
|
@@ -827,7 +864,7 @@ def guardian_rate(
|
|
|
827
864
|
if updated:
|
|
828
865
|
console.print(f"[green]✅ PR #{pr}: recorded {applied} applied findings.[/green]")
|
|
829
866
|
else:
|
|
830
|
-
console.print(f"[red]❌ No unrated entry found for PR #{pr} in {metrics}.[/red]")
|
|
867
|
+
console.print(f"[red]❌ No unrated entry found for PR #{pr} in {escape(metrics)}.[/red]")
|
|
831
868
|
raise typer.Exit(code=1)
|
|
832
869
|
|
|
833
870
|
|
|
@@ -839,7 +876,7 @@ def guardian_stats(
|
|
|
839
876
|
"""Show Guardian review quality metrics trend."""
|
|
840
877
|
reviews = load_reviews(Path(metrics))
|
|
841
878
|
if not reviews:
|
|
842
|
-
console.print(f"[yellow]No metrics found in {metrics}.[/yellow]")
|
|
879
|
+
console.print(f"[yellow]No metrics found in {escape(metrics)}.[/yellow]")
|
|
843
880
|
raise typer.Exit
|
|
844
881
|
|
|
845
882
|
reviews = reviews[-last:]
|
|
@@ -871,7 +908,7 @@ def guardian_stats(
|
|
|
871
908
|
lgtm = "✅" if r.get("lgtm") else ""
|
|
872
909
|
table.add_row(
|
|
873
910
|
pr_str,
|
|
874
|
-
str(r.get("model", "")),
|
|
911
|
+
escape(str(r.get("model", ""))),
|
|
875
912
|
f"{tokens:,}",
|
|
876
913
|
str(findings),
|
|
877
914
|
applied_str,
|
|
@@ -962,8 +999,8 @@ def _render_drift_table(reports: list[DriftReport]) -> None:
|
|
|
962
999
|
table.add_column("Status", justify="center")
|
|
963
1000
|
for r in reports:
|
|
964
1001
|
table.add_row(
|
|
965
|
-
r.fqn_prefix,
|
|
966
|
-
r.expected_pattern or "(hygiene)",
|
|
1002
|
+
escape(r.fqn_prefix),
|
|
1003
|
+
escape(r.expected_pattern or "(hygiene)"),
|
|
967
1004
|
f"{r.drift_score:.2f}",
|
|
968
1005
|
f"{r.tv_imports:.2f}" if r.tv_imports is not None else "—",
|
|
969
1006
|
f"{r.tv_calls:.2f}" if r.tv_calls is not None else "—",
|
|
@@ -1029,11 +1066,13 @@ def drift(
|
|
|
1029
1066
|
Exits with code 1 if any domain drift score meets or exceeds the critical threshold.
|
|
1030
1067
|
"""
|
|
1031
1068
|
if not Path(db).is_file():
|
|
1032
|
-
console.print(
|
|
1069
|
+
console.print(
|
|
1070
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
1071
|
+
)
|
|
1033
1072
|
raise typer.Exit(code=1)
|
|
1034
1073
|
|
|
1035
1074
|
if not Path(patterns).is_file():
|
|
1036
|
-
console.print(f"[bold red]❌ Patterns file not found:[/bold red] {patterns}")
|
|
1075
|
+
console.print(f"[bold red]❌ Patterns file not found:[/bold red] {escape(patterns)}")
|
|
1037
1076
|
raise typer.Exit(code=1)
|
|
1038
1077
|
|
|
1039
1078
|
try:
|
|
@@ -1041,7 +1080,7 @@ def drift(
|
|
|
1041
1080
|
db, patterns, max_drift=max_drift, profile=profile, max_residual=max_residual
|
|
1042
1081
|
)
|
|
1043
1082
|
except Exception as e:
|
|
1044
|
-
console.print(f"[bold red]❌ Error during drift analysis:[/bold red] {e}")
|
|
1083
|
+
console.print(f"[bold red]❌ Error during drift analysis:[/bold red] {escape(str(e))}")
|
|
1045
1084
|
raise typer.Exit(code=1) from e
|
|
1046
1085
|
|
|
1047
1086
|
if output_format == DriftOutputFormat.JSON:
|
|
@@ -1123,17 +1162,19 @@ def init_ontology(
|
|
|
1123
1162
|
) -> None:
|
|
1124
1163
|
"""Propose a starter patterns.yaml from the measured graph (measure-then-label)."""
|
|
1125
1164
|
if Path(out).exists() and not force:
|
|
1126
|
-
console.print(
|
|
1165
|
+
console.print(
|
|
1166
|
+
f"[bold red]❌ {escape(out)} already exists[/bold red] — use --force to overwrite."
|
|
1167
|
+
)
|
|
1127
1168
|
raise typer.Exit(code=1)
|
|
1128
1169
|
try:
|
|
1129
1170
|
text = propose_ontology(db, margin=margin, min_nodes=min_nodes, depth=depth)
|
|
1130
1171
|
except FileNotFoundError as e:
|
|
1131
|
-
console.print(f"[bold red]❌ {e}[/bold red]")
|
|
1172
|
+
console.print(f"[bold red]❌ {escape(str(e))}[/bold red]")
|
|
1132
1173
|
raise typer.Exit(code=1) from e
|
|
1133
1174
|
Path(out).write_text(text)
|
|
1134
1175
|
_render_init_summary(text)
|
|
1135
|
-
console.print(f"[bold green]✅ Proposed ontology written to {out}[/bold green]")
|
|
1136
|
-
console.print(f"Next: [cyan]cgis drift --db {db} --patterns {out}[/cyan]")
|
|
1176
|
+
console.print(f"[bold green]✅ Proposed ontology written to {escape(out)}[/bold green]")
|
|
1177
|
+
console.print(f"Next: [cyan]cgis drift --db {escape(db)} --patterns {escape(out)}[/cyan]")
|
|
1137
1178
|
|
|
1138
1179
|
|
|
1139
1180
|
@app.command()
|
|
@@ -1163,7 +1204,9 @@ def context(
|
|
|
1163
1204
|
"""
|
|
1164
1205
|
path = Path(db)
|
|
1165
1206
|
if not path.is_file():
|
|
1166
|
-
console.print(
|
|
1207
|
+
console.print(
|
|
1208
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
1209
|
+
)
|
|
1167
1210
|
raise typer.Exit(code=1)
|
|
1168
1211
|
|
|
1169
1212
|
err_console = Console(stderr=True)
|
|
@@ -1171,16 +1214,18 @@ def context(
|
|
|
1171
1214
|
resolution = resolve_fqn(store, fqn)
|
|
1172
1215
|
if resolution.resolved is None:
|
|
1173
1216
|
if resolution.candidates:
|
|
1174
|
-
err_console.print(f"[bold red]❌ Ambiguous FQN:[/bold red] {fqn}")
|
|
1217
|
+
err_console.print(f"[bold red]❌ Ambiguous FQN:[/bold red] {escape(fqn)}")
|
|
1175
1218
|
for candidate in resolution.candidates:
|
|
1176
|
-
err_console.print(f" [dim]- {candidate}[/dim]")
|
|
1219
|
+
err_console.print(f" [dim]- {escape(candidate)}[/dim]")
|
|
1177
1220
|
if resolution.truncated:
|
|
1178
1221
|
err_console.print(_TRUNCATED_HINT)
|
|
1179
1222
|
else:
|
|
1180
|
-
err_console.print(f"[bold red]❌ Node not found in graph:[/bold red] {fqn}")
|
|
1223
|
+
err_console.print(f"[bold red]❌ Node not found in graph:[/bold red] {escape(fqn)}")
|
|
1181
1224
|
raise typer.Exit(code=1)
|
|
1182
1225
|
if resolution.via_suffix:
|
|
1183
|
-
err_console.print(
|
|
1226
|
+
err_console.print(
|
|
1227
|
+
f"[dim]Resolved '{escape(fqn)}' → '{escape(resolution.resolved)}'[/dim]"
|
|
1228
|
+
)
|
|
1184
1229
|
payload = build_context(store, resolution.resolved, depth=depth, source_root=source_root)
|
|
1185
1230
|
typer.echo(payload)
|
|
1186
1231
|
|
|
@@ -1193,14 +1238,16 @@ def _render_metrics(report: ArchitectureReport) -> None:
|
|
|
1193
1238
|
bottlenecks.add_column("In", justify="right", style="green")
|
|
1194
1239
|
bottlenecks.add_column("Out", justify="right", style="yellow")
|
|
1195
1240
|
for m in report.bottlenecks:
|
|
1196
|
-
bottlenecks.add_row(
|
|
1241
|
+
bottlenecks.add_row(
|
|
1242
|
+
escape(m.node_id), escape(m.node_type), str(m.in_degree), str(m.out_degree)
|
|
1243
|
+
)
|
|
1197
1244
|
console.print(bottlenecks)
|
|
1198
1245
|
|
|
1199
1246
|
gods = Table(title="🏛️ God classes (top by declared members)")
|
|
1200
1247
|
gods.add_column("Class", style="cyan")
|
|
1201
1248
|
gods.add_column("Members", justify="right", style="red")
|
|
1202
1249
|
for m in report.god_classes:
|
|
1203
|
-
gods.add_row(m.node_id, str(m.out_degree))
|
|
1250
|
+
gods.add_row(escape(m.node_id), str(m.out_degree))
|
|
1204
1251
|
console.print(gods)
|
|
1205
1252
|
|
|
1206
1253
|
critical = Table(title="⭐ Critical nodes (top by PageRank — transitive importance)")
|
|
@@ -1213,7 +1260,11 @@ def _render_metrics(report: ArchitectureReport) -> None:
|
|
|
1213
1260
|
# In/Out are over the same internal graph PageRank ran on; a high rank with
|
|
1214
1261
|
# In=0 is a dangling-mass leaf artifact, not a real hub (#237).
|
|
1215
1262
|
critical.add_row(
|
|
1216
|
-
m.node_id
|
|
1263
|
+
escape(m.node_id),
|
|
1264
|
+
escape(m.node_type),
|
|
1265
|
+
f"{m.page_rank:.4f}",
|
|
1266
|
+
str(m.in_degree),
|
|
1267
|
+
str(m.out_degree),
|
|
1217
1268
|
)
|
|
1218
1269
|
console.print(critical)
|
|
1219
1270
|
|
|
@@ -1245,7 +1296,9 @@ def metrics(
|
|
|
1245
1296
|
console.print("[bold red]❌ metrics supports --format text or json only.[/bold red]")
|
|
1246
1297
|
raise typer.Exit(code=2)
|
|
1247
1298
|
if not Path(db).is_file():
|
|
1248
|
-
console.print(
|
|
1299
|
+
console.print(
|
|
1300
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
1301
|
+
)
|
|
1249
1302
|
raise typer.Exit(code=1)
|
|
1250
1303
|
try:
|
|
1251
1304
|
with DuckDBAnalyzer(db) as analyzer:
|
|
@@ -1253,7 +1306,7 @@ def metrics(
|
|
|
1253
1306
|
bottleneck_limit=limit, god_limit=limit, critical_limit=limit, exclude=exclude
|
|
1254
1307
|
)
|
|
1255
1308
|
except Exception as e: # duckdb missing, extension fetch, or a non-SQLite file
|
|
1256
|
-
console.print(f"[bold red]❌ {e}[/bold red]")
|
|
1309
|
+
console.print(f"[bold red]❌ {escape(str(e))}[/bold red]")
|
|
1257
1310
|
raise typer.Exit(code=1) from e
|
|
1258
1311
|
|
|
1259
1312
|
if output_format == OutputFormat.JSON:
|
|
@@ -1271,16 +1324,20 @@ def _resolve_checkpoint(store: SQLiteStore, target: str) -> str:
|
|
|
1271
1324
|
resolution = resolve_fqn(store, target)
|
|
1272
1325
|
if resolution.resolved is None:
|
|
1273
1326
|
if resolution.candidates:
|
|
1274
|
-
err_console.print(f"[bold red]❌ Ambiguous checkpoint FQN:[/bold red] {target}")
|
|
1327
|
+
err_console.print(f"[bold red]❌ Ambiguous checkpoint FQN:[/bold red] {escape(target)}")
|
|
1275
1328
|
for candidate in resolution.candidates:
|
|
1276
|
-
err_console.print(f" [dim]- {candidate}[/dim]")
|
|
1329
|
+
err_console.print(f" [dim]- {escape(candidate)}[/dim]")
|
|
1277
1330
|
if resolution.truncated:
|
|
1278
1331
|
err_console.print(_TRUNCATED_HINT)
|
|
1279
1332
|
else:
|
|
1280
|
-
err_console.print(
|
|
1333
|
+
err_console.print(
|
|
1334
|
+
f"[bold red]❌ Checkpoint not found in graph:[/bold red] {escape(target)}"
|
|
1335
|
+
)
|
|
1281
1336
|
raise typer.Exit(code=1)
|
|
1282
1337
|
if resolution.via_suffix:
|
|
1283
|
-
err_console.print(
|
|
1338
|
+
err_console.print(
|
|
1339
|
+
f"[dim]Resolved '{escape(target)}' → '{escape(resolution.resolved)}'[/dim]"
|
|
1340
|
+
)
|
|
1284
1341
|
return resolution.resolved
|
|
1285
1342
|
|
|
1286
1343
|
|
|
@@ -1347,7 +1404,9 @@ def audit(
|
|
|
1347
1404
|
)
|
|
1348
1405
|
raise typer.Exit(code=2)
|
|
1349
1406
|
if not Path(db).is_file():
|
|
1350
|
-
console.print(
|
|
1407
|
+
console.print(
|
|
1408
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
1409
|
+
)
|
|
1351
1410
|
raise typer.Exit(code=1)
|
|
1352
1411
|
|
|
1353
1412
|
with SQLiteStore(db) as store:
|
|
@@ -1434,12 +1493,14 @@ def suggest_packages_cmd(
|
|
|
1434
1493
|
always exits 0 on success. Run `ingest` first.
|
|
1435
1494
|
"""
|
|
1436
1495
|
if not Path(db).is_file():
|
|
1437
|
-
console.print(
|
|
1496
|
+
console.print(
|
|
1497
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
1498
|
+
)
|
|
1438
1499
|
raise typer.Exit(code=1)
|
|
1439
1500
|
try:
|
|
1440
1501
|
report = suggest_packages(db, prefix, with_calls=with_calls, min_q=min_q)
|
|
1441
1502
|
except Exception as e:
|
|
1442
|
-
console.print(f"[bold red]❌ Error during suggest-packages:[/bold red] {e}")
|
|
1503
|
+
console.print(f"[bold red]❌ Error during suggest-packages:[/bold red] {escape(str(e))}")
|
|
1443
1504
|
raise typer.Exit(code=1) from e
|
|
1444
1505
|
|
|
1445
1506
|
if output_format == SuggestOutputFormat.JSON:
|
|
@@ -1472,7 +1533,7 @@ def _render_fractal(reports: list[FractalReport]) -> None:
|
|
|
1472
1533
|
entropy = "—" if rung.entropy is None else f"{rung.entropy:.2f}"
|
|
1473
1534
|
name = rung.name if rung.live else f"{rung.name} [dim](no_signal)[/dim]"
|
|
1474
1535
|
table.add_row(
|
|
1475
|
-
name,
|
|
1536
|
+
escape(name),
|
|
1476
1537
|
str(rung.groups),
|
|
1477
1538
|
str(rung.triads),
|
|
1478
1539
|
entropy,
|
|
@@ -1500,10 +1561,12 @@ def fractal(
|
|
|
1500
1561
|
try:
|
|
1501
1562
|
reports = analyze_fractal_db(db)
|
|
1502
1563
|
except FileNotFoundError as e:
|
|
1503
|
-
console.print(
|
|
1564
|
+
console.print(
|
|
1565
|
+
f"[bold red]❌ Database not found:[/bold red] {escape(db)}. Run `ingest` first."
|
|
1566
|
+
)
|
|
1504
1567
|
raise typer.Exit(code=1) from e
|
|
1505
1568
|
except Exception as e:
|
|
1506
|
-
console.print(f"[bold red]❌ Error during fractal analysis:[/bold red] {e}")
|
|
1569
|
+
console.print(f"[bold red]❌ Error during fractal analysis:[/bold red] {escape(str(e))}")
|
|
1507
1570
|
raise typer.Exit(code=1) from e
|
|
1508
1571
|
|
|
1509
1572
|
if output_format == FractalOutputFormat.JSON:
|
|
@@ -9,7 +9,9 @@ measured values plus a margin — green by construction on the same graph.
|
|
|
9
9
|
import math
|
|
10
10
|
import tempfile
|
|
11
11
|
from collections import Counter
|
|
12
|
+
from functools import lru_cache
|
|
12
13
|
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
13
15
|
|
|
14
16
|
import yaml
|
|
15
17
|
|
|
@@ -203,6 +205,61 @@ def _floor2(x: float) -> float:
|
|
|
203
205
|
return math.floor(x * 100) / 100
|
|
204
206
|
|
|
205
207
|
|
|
208
|
+
@lru_cache(maxsize=1)
|
|
209
|
+
def _bundled_templates() -> dict[str, Any]:
|
|
210
|
+
"""Return the ``patterns:`` block of the bundled header we are about to emit.
|
|
211
|
+
|
|
212
|
+
Parsed from ``_DEFAULT_ONTOLOGY_HEADER`` rather than from the repo's own
|
|
213
|
+
patterns.yaml so the generated overrides can never disagree with the
|
|
214
|
+
generated templates — both come from the same string.
|
|
215
|
+
"""
|
|
216
|
+
raw = yaml.safe_load(_DEFAULT_ONTOLOGY_HEADER) or {}
|
|
217
|
+
patterns = raw.get("patterns") or {}
|
|
218
|
+
return patterns if isinstance(patterns, dict) else {}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _depth_param_override(template_name: str, measured_depth: int) -> str | None:
|
|
222
|
+
"""Return a ``params:`` line pinning a shallower-than-declared min depth (#229).
|
|
223
|
+
|
|
224
|
+
``layered_dag`` gates ``dag_depth: {min: $min_depth}`` with a declared default
|
|
225
|
+
of 3, but plenty of real architectures have exactly two meaningful layers
|
|
226
|
+
(``types → registry → content`` and JSX component trees both bottom out at 2).
|
|
227
|
+
Emitting the template default unqualified makes the very first drift report on
|
|
228
|
+
a fresh repo read ``dag_depth 2.0 < min 3.0`` — a violation that is
|
|
229
|
+
architecturally wrong and erodes trust in the tool on first use.
|
|
230
|
+
|
|
231
|
+
Pinning the *measured* depth mirrors ``_baseline_lines``: acknowledge what is
|
|
232
|
+
there, keep the gate live for future regressions. Domains that already clear
|
|
233
|
+
the declared default get no override — the bar stays where the template put it.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
template_name: Name of the bound template (e.g. ``"layered_dag"``).
|
|
237
|
+
measured_depth: The domain's measured ``dag_depth``.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
A YAML ``params:`` line, or None when the template declares no
|
|
241
|
+
``$param``-driven depth gate or the domain already meets it.
|
|
242
|
+
"""
|
|
243
|
+
template = _bundled_templates().get(template_name) or {}
|
|
244
|
+
constraint = template.get("dag_depth")
|
|
245
|
+
if not isinstance(constraint, dict) or "min" not in constraint:
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
reference = constraint["min"]
|
|
249
|
+
if not (isinstance(reference, str) and reference.startswith("$")):
|
|
250
|
+
return None # a literal bound is not overridable via params
|
|
251
|
+
|
|
252
|
+
param = reference[1:]
|
|
253
|
+
declared = (template.get("params") or {}).get(param)
|
|
254
|
+
if declared is None or measured_depth >= float(declared):
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
f" params: {{{param}: {measured_depth}}}"
|
|
259
|
+
f" # measured depth — {measured_depth} layers is this domain's real shape"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
206
263
|
def _hygiene_score(
|
|
207
264
|
fp: PatternFingerprint, prefix: str, scorer: DriftScorer, profile: str | None
|
|
208
265
|
) -> float:
|
|
@@ -329,6 +386,14 @@ def _domain_entry(
|
|
|
329
386
|
)
|
|
330
387
|
lines.append(f" expected_pattern: {best_name}")
|
|
331
388
|
lines.append(f" profile: {profile}{profile_suffix}")
|
|
389
|
+
|
|
390
|
+
# A labeled domain shallower than its template's declared depth gate pins the
|
|
391
|
+
# measured depth, so the proposal stays green on its own graph (#229).
|
|
392
|
+
if reason is None:
|
|
393
|
+
depth_override = _depth_param_override(best_name, fp.dag_depth)
|
|
394
|
+
if depth_override is not None:
|
|
395
|
+
lines.append(depth_override)
|
|
396
|
+
|
|
332
397
|
lines.append(f" drift_tolerance: {tolerance:.2f} {comment}")
|
|
333
398
|
|
|
334
399
|
# Emit hygiene_baseline for any measured value that breaches the GLOBAL hygiene bound
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-brain
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -133,7 +133,7 @@ The fastest path — no config files, no paths to wire up:
|
|
|
133
133
|
/plugin install cgis@codegraph-brain
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
That ships the MCP server, a skill that teaches the agent *when* to query the graph instead of reading files, and `/cgis
|
|
136
|
+
That ships the MCP server, a skill that teaches the agent *when* to query the graph instead of reading files, and `/cgis:ingest` to build the graph on first use. The server is pulled from PyPI on demand via `uvx`, so there is nothing to clone or build.
|
|
137
137
|
|
|
138
138
|
---
|
|
139
139
|
|
|
@@ -235,6 +235,20 @@ classDef externalNode fill:#fff3e0,stroke:#e65100,stroke-width:1px,stroke-dashar
|
|
|
235
235
|
|
|
236
236
|
---
|
|
237
237
|
|
|
238
|
+
## 💼 Architecture Audit
|
|
239
|
+
|
|
240
|
+
CGIS is free and you can run it yourself. If you would rather have the analysis than the tool, I run a fixed-price audit of your codebase's structure — authorisation coverage, blast radius, coupling, architectural drift — delivered in five working days.
|
|
241
|
+
|
|
242
|
+
**[Read what's included →](docs/AUDIT.md)** — $2,400 fixed, with an explicit list of what the analysis cannot see.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 🔒 Privacy
|
|
247
|
+
|
|
248
|
+
CGIS collects nothing: no telemetry, no analytics, no account. Your code and the graph built from it stay on your machine. See [PRIVACY.md](PRIVACY.md).
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
238
252
|
## 🛠️ Development
|
|
239
253
|
|
|
240
254
|
### Requirements
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
cgis/__init__.py,sha256=3QdRGRHRQHrjQYXrPCo1uCZKcx5C-f098rpT21gq3RU,271
|
|
2
2
|
cgis/__main__.py,sha256=w9qXbHyKaCVxkdm7i_NDSc-Mz07ivIoaAAVFGvs3Plo,326
|
|
3
|
-
cgis/cli.py,sha256=
|
|
3
|
+
cgis/cli.py,sha256=Ltw4o_rr4YqxaidJVTuoNNHTd9HBD3JUJlRXiZiJOcs,59440
|
|
4
4
|
cgis/pipeline.py,sha256=AJfBhlH5ZD4wkDX6QMScuDPbl_Wwr_ZFplNzfoorsqg,10529
|
|
5
5
|
cgis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
cgis/api/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -56,7 +56,7 @@ cgis/query/drift/drift.py,sha256=7ku8cOp8i59J-KvBWQedNxBc0bu9P-CGXM12CuxpQF0,364
|
|
|
56
56
|
cgis/query/drift/drift_service.py,sha256=-TKtU1Tml5vg2zUy9rnP2QuP7kTyg9hTLYDx7N4rLlM,9536
|
|
57
57
|
cgis/query/drift/fingerprint.py,sha256=UmN3viPZceU2JH-zIf5CvIpbV4kBiaL5m7jObxZDPBc,10658
|
|
58
58
|
cgis/query/drift/fractal.py,sha256=1KkXGTmkYgOVum67M-BG_YUsTdorGTzP4kAJmrkNwKM,10205
|
|
59
|
-
cgis/query/drift/ontology_init.py,sha256=
|
|
59
|
+
cgis/query/drift/ontology_init.py,sha256=cWcr_UjUJRV9w8MGZITt4m6kvQ3asbKLZaN402Eltoo,21113
|
|
60
60
|
cgis/query/drift/quotient.py,sha256=4tV29yk07VYfbrAo7PIrWQ4BagZ0T6nItWP_8Macx_4,2711
|
|
61
61
|
cgis/query/drift/triads.py,sha256=OoGxSAsU9QzHP2MboYkXJ3Jg7PyoE9tO6BfX65fwyYQ,6150
|
|
62
62
|
cgis/query/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -71,8 +71,8 @@ cgis/resolver/symbols.py,sha256=6PNDIgoJ2Mj2xZ4XHpEXwjHe76fi9zvOwF7tAON1GBw,8513
|
|
|
71
71
|
cgis/resolver/uplift.py,sha256=Hkye1q-kI-gKO6GIBYv3gUZj1AC0-_mkpoIpmwVsG4g,9591
|
|
72
72
|
cgis/storage/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
73
|
cgis/storage/sqlite_store.py,sha256=Dkxkl0WDyTakZFfxWuVpYLNHA_H7CMvRkSUvev3MHew,24778
|
|
74
|
-
codegraph_brain-0.7.
|
|
75
|
-
codegraph_brain-0.7.
|
|
76
|
-
codegraph_brain-0.7.
|
|
77
|
-
codegraph_brain-0.7.
|
|
78
|
-
codegraph_brain-0.7.
|
|
74
|
+
codegraph_brain-0.7.2.dist-info/METADATA,sha256=X-WwNgfAAQAWmhnG54PifcCM_t70jx9O-F7PISIO6O8,13617
|
|
75
|
+
codegraph_brain-0.7.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
76
|
+
codegraph_brain-0.7.2.dist-info/entry_points.txt,sha256=G_ekY569jca0ubm-I3gA9Id9xRj8fc42CDgLgTC-5L4,77
|
|
77
|
+
codegraph_brain-0.7.2.dist-info/licenses/LICENSE,sha256=1X36nOG5VocDaEGzAZqIv7uawy_7sWlFoiwECwx5aBI,1065
|
|
78
|
+
codegraph_brain-0.7.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|