codegraph-brain 0.7.1__py3-none-any.whl → 0.7.3__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 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(f"[bold red]❌ Domains config file not found:[/bold red] {domains}")
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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(node.name, node.type.value, node.id, f"{node.file_path}:{node.start_line}")
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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(f"[bold red]❌ {out} already exists[/bold red] — use --force to overwrite.")
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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(f"[dim]Resolved '{fqn}' → '{resolution.resolved}'[/dim]")
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(m.node_id, m.node_type, str(m.in_degree), str(m.out_degree))
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, m.node_type, f"{m.page_rank:.4f}", str(m.in_degree), str(m.out_degree)
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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(f"[bold red]❌ Checkpoint not found in graph:[/bold red] {target}")
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(f"[dim]Resolved '{target}' → '{resolution.resolved}'[/dim]")
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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(f"[bold red]❌ Database not found:[/bold red] {db}. Run `ingest` first.")
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.1
3
+ Version: 0.7.3
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,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=7Nv1feCj-Fb-glckdvN5xR7Prx_rdc-eJrrxYRIUU9A,57495
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=mdCBV3guHs72aTMzjyXf1JLcD9qHDJEZwpzmZzIRYxY,18302
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.1.dist-info/METADATA,sha256=RKBQO81Sn7Pk0J0JE_0Qke1FueB17dFZbg2E0z89vlA,13617
75
- codegraph_brain-0.7.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
76
- codegraph_brain-0.7.1.dist-info/entry_points.txt,sha256=G_ekY569jca0ubm-I3gA9Id9xRj8fc42CDgLgTC-5L4,77
77
- codegraph_brain-0.7.1.dist-info/licenses/LICENSE,sha256=1X36nOG5VocDaEGzAZqIv7uawy_7sWlFoiwECwx5aBI,1065
78
- codegraph_brain-0.7.1.dist-info/RECORD,,
74
+ codegraph_brain-0.7.3.dist-info/METADATA,sha256=Qk6-V9Em57bTdZByMD18KzvWSZ25ecQZvA8NGG1kwS4,13617
75
+ codegraph_brain-0.7.3.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
76
+ codegraph_brain-0.7.3.dist-info/entry_points.txt,sha256=G_ekY569jca0ubm-I3gA9Id9xRj8fc42CDgLgTC-5L4,77
77
+ codegraph_brain-0.7.3.dist-info/licenses/LICENSE,sha256=1X36nOG5VocDaEGzAZqIv7uawy_7sWlFoiwECwx5aBI,1065
78
+ codegraph_brain-0.7.3.dist-info/RECORD,,