codegraph-engine 2.1.7__py3-none-any.whl → 2.1.8__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.
codegraph/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """CodeGraph MCP: local, evidence-backed codebase intelligence."""
2
2
 
3
- __version__ = "2.1.7"
3
+ __version__ = "2.1.8"
4
4
  from codegraph.errors import (
5
5
  CodeGraphError,
6
6
  ErrorCode,
codegraph/cli.py CHANGED
@@ -9,6 +9,7 @@ import typer
9
9
 
10
10
  from codegraph import __version__
11
11
  from codegraph.architecture import get_architecture
12
+ from codegraph.cli_output import cli_echo
12
13
  from codegraph.config import Settings
13
14
  from codegraph.context import get_context
14
15
  from codegraph.errors import ErrorCode, SecurityError
@@ -605,8 +606,10 @@ def serve(
605
606
  """Run the stdio MCP server (requires the optional mcp extra)."""
606
607
  repo = _resolve_repo(path, repository)
607
608
  from codegraph.mcp import create_server
609
+ from codegraph.process_lifecycle import run_mcp_stdio_server
608
610
 
609
- create_server(repo, profile=profile).run()
611
+ server = create_server(repo, profile=profile)
612
+ run_mcp_stdio_server(server, repo, profile=profile)
610
613
 
611
614
 
612
615
  mcp_app = typer.Typer(help="MCP server commands.")
@@ -622,8 +625,70 @@ def mcp_serve(
622
625
  """Run the stdio MCP server (requires the optional mcp extra)."""
623
626
  repo = _resolve_repo(path, repository)
624
627
  from codegraph.mcp import create_server
628
+ from codegraph.process_lifecycle import run_mcp_stdio_server
625
629
 
626
- create_server(repo, profile=profile).run()
630
+ server = create_server(repo, profile=profile)
631
+ run_mcp_stdio_server(server, repo, profile=profile)
632
+
633
+
634
+ def _execute_stop(
635
+ path: Path | None,
636
+ repository: Path | None,
637
+ all_processes: bool,
638
+ timeout: float,
639
+ json_output: bool,
640
+ ) -> None:
641
+ from codegraph.process_lifecycle import stop_codegraph_processes
642
+
643
+ target_repo: Path | None = None
644
+ if not all_processes:
645
+ target_repo = _resolve_repo(path, repository)
646
+
647
+ res = stop_codegraph_processes(
648
+ repository=target_repo,
649
+ stop_all=all_processes,
650
+ timeout_sec=timeout,
651
+ )
652
+ if json_output:
653
+ cli_echo(json.dumps(res.as_dict(), indent=2), json_mode=True)
654
+ else:
655
+ cli_echo(res.format_human())
656
+
657
+
658
+ @app.command("stop")
659
+ def stop_cmd(
660
+ path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
661
+ repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
662
+ all_processes: Annotated[bool, typer.Option("--all", "-a", help="Stop all CodeGraph-owned MCP processes across all repositories")] = False,
663
+ timeout: Annotated[float, typer.Option("--timeout", help="Graceful shutdown timeout in seconds before force-kill")] = 3.0,
664
+ json_output: Annotated[bool, typer.Option("--json", help="Output deterministic machine-readable JSON")] = False,
665
+ ) -> None:
666
+ """Safely stop running CodeGraph MCP background processes and release file locks."""
667
+ _execute_stop(path, repository, all_processes, timeout, json_output)
668
+
669
+
670
+ @mcp_app.command("stop")
671
+ def mcp_stop_cmd(
672
+ path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
673
+ repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
674
+ all_processes: Annotated[bool, typer.Option("--all", "-a", help="Stop all CodeGraph-owned MCP processes across all repositories")] = False,
675
+ timeout: Annotated[float, typer.Option("--timeout", help="Graceful shutdown timeout in seconds before force-kill")] = 3.0,
676
+ json_output: Annotated[bool, typer.Option("--json", help="Output deterministic machine-readable JSON")] = False,
677
+ ) -> None:
678
+ """Safely stop running CodeGraph MCP background processes and release file locks."""
679
+ _execute_stop(path, repository, all_processes, timeout, json_output)
680
+
681
+
682
+ @mcp_app.command("kill")
683
+ def mcp_kill_cmd(
684
+ path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
685
+ repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
686
+ all_processes: Annotated[bool, typer.Option("--all", "-a", help="Stop all CodeGraph-owned MCP processes across all repositories")] = False,
687
+ timeout: Annotated[float, typer.Option("--timeout", help="Graceful shutdown timeout in seconds before force-kill")] = 1.0,
688
+ json_output: Annotated[bool, typer.Option("--json", help="Output deterministic machine-readable JSON")] = False,
689
+ ) -> None:
690
+ """Alias for `codegraph stop` to terminate CodeGraph MCP processes and release locks."""
691
+ _execute_stop(path, repository, all_processes, timeout, json_output)
627
692
 
628
693
 
629
694
  @mcp_app.command("doctor")
@@ -636,9 +701,9 @@ def mcp_doctor(
636
701
 
637
702
  report = run_mcp_doctor(profile=profile)
638
703
  if json_output:
639
- typer.echo(json.dumps(report.as_dict(), indent=2))
704
+ cli_echo(json.dumps(report.as_dict(), indent=2), json_mode=True)
640
705
  else:
641
- typer.echo(report.format_human())
706
+ cli_echo(report.format_human())
642
707
  if not report.healthy:
643
708
  raise typer.Exit(code=1)
644
709
 
@@ -656,9 +721,9 @@ def mcp_config_check(
656
721
  ws = (path or Path.cwd()).resolve()
657
722
  rep = check_mcp_configuration(workspace_dir=ws, config_file=config_file, include_example=include_example)
658
723
  if json_output:
659
- typer.echo(json.dumps(rep.as_dict(), indent=2))
724
+ cli_echo(json.dumps(rep.as_dict(), indent=2), json_mode=True)
660
725
  else:
661
- typer.echo(f"{rep.status.value}: {rep.message}")
726
+ cli_echo(f"{rep.status.value}: {rep.message}")
662
727
 
663
728
 
664
729
  @mcp_app.command("capabilities")
@@ -666,7 +731,7 @@ def mcp_capabilities() -> None:
666
731
  """Output the deterministic machine-readable CodeGraph capability manifest."""
667
732
  from codegraph.agent_capabilities import get_capability_manifest
668
733
 
669
- typer.echo(json.dumps(get_capability_manifest(), indent=2))
734
+ cli_echo(json.dumps(get_capability_manifest(), indent=2), json_mode=True)
670
735
 
671
736
 
672
737
  @mcp_app.command("rules")
@@ -676,7 +741,7 @@ def mcp_rules(
676
741
  """Render deterministic CodeGraph agent rules for the specified agent."""
677
742
  from codegraph.agent_rules import render_agent_rules
678
743
 
679
- typer.echo(render_agent_rules(agent=agent), nl=False)
744
+ cli_echo(render_agent_rules(agent=agent), nl=False)
680
745
 
681
746
 
682
747
  @app.command()
@@ -685,9 +750,20 @@ def doctor(
685
750
  repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
686
751
  resources: Annotated[bool, typer.Option("--resources")] = False,
687
752
  database: Annotated[bool, typer.Option("--database", help="Run comprehensive database integrity and schema checks")] = False,
753
+ processes: Annotated[bool, typer.Option("--processes", help="Inspect active CodeGraph processes, parent state, and stale PID files")] = False,
688
754
  json_output: Annotated[bool, typer.Option("--json")] = False,
689
755
  ) -> None:
690
- """Check repository and index readiness, database integrity, and freshness."""
756
+ """Check repository and index readiness, database integrity, process lifecycle, and freshness."""
757
+ if processes:
758
+ from codegraph.process_lifecycle import discover_codegraph_processes
759
+
760
+ discovery = discover_codegraph_processes(clean_stale=True)
761
+ if json_output:
762
+ cli_echo(json.dumps(discovery.as_dict(), indent=2), json_mode=True)
763
+ else:
764
+ cli_echo(discovery.format_human())
765
+ return
766
+
691
767
  repo = _resolve_repo(path, repository)
692
768
  indexer_inst = _indexer(repo)
693
769
  db = repo / ".codegraph.sqlite3"
@@ -1007,7 +1083,7 @@ def install(
1007
1083
 
1008
1084
  rules_txt = render_agent_rules(canon)
1009
1085
  if json_output:
1010
- typer.echo(
1086
+ cli_echo(
1011
1087
  json.dumps(
1012
1088
  {
1013
1089
  "status": "ok",
@@ -1016,21 +1092,22 @@ def install(
1016
1092
  "instructions_block": format_marker_block(rules_txt),
1017
1093
  },
1018
1094
  indent=2,
1019
- )
1095
+ ),
1096
+ json_mode=True,
1020
1097
  )
1021
1098
  else:
1022
- typer.echo(f"# MCP Configuration ({canon})\n")
1023
- typer.echo(json.dumps(snippet, indent=2))
1024
- typer.echo(f"\n# Instructions Block ({canon})\n")
1025
- typer.echo(format_marker_block(rules_txt), nl=False)
1099
+ cli_echo(f"# MCP Configuration ({canon})\n")
1100
+ cli_echo(json.dumps(snippet, indent=2), json_mode=True)
1101
+ cli_echo(f"\n# Instructions Block ({canon})\n")
1102
+ cli_echo(format_marker_block(rules_txt), nl=False)
1026
1103
  return
1027
1104
  except CodeGraphError as exc:
1028
1105
  if json_output:
1029
- typer.echo(json.dumps(exc.to_response(), indent=2))
1106
+ cli_echo(json.dumps(exc.to_response(), indent=2), json_mode=True)
1030
1107
  else:
1031
- typer.echo(f"Error [{exc.code}]: {exc.message}")
1108
+ cli_echo(f"Error [{exc.code}]: {exc.message}")
1032
1109
  if exc.next_action and exc.next_action.get("command"):
1033
- typer.echo(f"\nTry:\n {exc.next_action['command']}")
1110
+ cli_echo(f"\nTry:\n {exc.next_action['command']}")
1034
1111
  raise typer.Exit(code=1) from exc
1035
1112
 
1036
1113
  ws = _resolve_repo(path, repository)
@@ -1040,15 +1117,15 @@ def install(
1040
1117
 
1041
1118
  assert isinstance(preview_rep, InstallerReport)
1042
1119
  if not json_output:
1043
- typer.echo(preview_rep.format_preview_human())
1044
- typer.echo("")
1045
- typer.echo("Continue? [y/N] ", nl=False)
1120
+ cli_echo(preview_rep.format_preview_human())
1121
+ cli_echo("")
1122
+ cli_echo("Continue? [y/N] ", nl=False)
1046
1123
  try:
1047
1124
  ans = input().strip().lower()
1048
1125
  except EOFError:
1049
1126
  ans = ""
1050
1127
  if not json_output:
1051
- typer.echo("")
1128
+ cli_echo("")
1052
1129
  return ans in ("y", "yes")
1053
1130
 
1054
1131
  try:
@@ -1062,22 +1139,22 @@ def install(
1062
1139
  )
1063
1140
  except CodeGraphError as exc:
1064
1141
  if json_output:
1065
- typer.echo(json.dumps(exc.to_response(), indent=2))
1142
+ cli_echo(json.dumps(exc.to_response(), indent=2), json_mode=True)
1066
1143
  else:
1067
- typer.echo(f"Error [{exc.code}]: {exc.message}")
1144
+ cli_echo(f"Error [{exc.code}]: {exc.message}")
1068
1145
  if exc.next_action and exc.next_action.get("reason"):
1069
- typer.echo(f"\nExpected:\n {exc.next_action['reason']}")
1146
+ cli_echo(f"\nExpected:\n {exc.next_action['reason']}")
1070
1147
  if exc.next_action and exc.next_action.get("command"):
1071
- typer.echo(f"\nTry:\n {exc.next_action['command']}")
1148
+ cli_echo(f"\nTry:\n {exc.next_action['command']}")
1072
1149
  raise typer.Exit(code=1) from exc
1073
1150
 
1074
1151
  if json_output:
1075
- typer.echo(json.dumps(report.as_dict(), indent=2))
1152
+ cli_echo(json.dumps(report.as_dict(), indent=2), json_mode=True)
1076
1153
  else:
1077
1154
  if yes and not dry_run:
1078
- typer.echo(report.format_preview_human())
1079
- typer.echo("")
1080
- typer.echo(report.format_human())
1155
+ cli_echo(report.format_preview_human())
1156
+ cli_echo("")
1157
+ cli_echo(report.format_human())
1081
1158
 
1082
1159
  if report.status == "error":
1083
1160
  raise typer.Exit(code=1)
@@ -1107,17 +1184,17 @@ def uninstall(
1107
1184
 
1108
1185
  def _confirm_uninstall(preview_rep: InstallerReport) -> bool:
1109
1186
  if not json_output:
1110
- typer.echo("CodeGraph Uninstaller\n")
1187
+ cli_echo("CodeGraph Uninstaller\n")
1111
1188
  for c in preview_rep.changes:
1112
1189
  if c.action in ("remove", "modify"):
1113
- typer.echo(f" - {c.display_path} ({c.action})")
1114
- typer.echo("\nContinue? [y/N] ", nl=False)
1190
+ cli_echo(f" - {c.display_path} ({c.action})")
1191
+ cli_echo("\nContinue? [y/N] ", nl=False)
1115
1192
  try:
1116
1193
  ans = input().strip().lower()
1117
1194
  except EOFError:
1118
1195
  ans = ""
1119
1196
  if not json_output:
1120
- typer.echo("")
1197
+ cli_echo("")
1121
1198
  return ans in ("y", "yes")
1122
1199
 
1123
1200
  try:
@@ -1130,17 +1207,17 @@ def uninstall(
1130
1207
  )
1131
1208
  except CodeGraphError as exc:
1132
1209
  if json_output:
1133
- typer.echo(json.dumps(exc.to_response(), indent=2))
1210
+ cli_echo(json.dumps(exc.to_response(), indent=2), json_mode=True)
1134
1211
  else:
1135
- typer.echo(f"Error [{exc.code}]: {exc.message}")
1212
+ cli_echo(f"Error [{exc.code}]: {exc.message}")
1136
1213
  if exc.next_action and exc.next_action.get("command"):
1137
- typer.echo(f"\nTry:\n {exc.next_action['command']}")
1214
+ cli_echo(f"\nTry:\n {exc.next_action['command']}")
1138
1215
  raise typer.Exit(code=1) from exc
1139
1216
 
1140
1217
  if json_output:
1141
- typer.echo(json.dumps(report.as_dict(), indent=2))
1218
+ cli_echo(json.dumps(report.as_dict(), indent=2), json_mode=True)
1142
1219
  else:
1143
- typer.echo(report.format_human())
1220
+ cli_echo(report.format_human())
1144
1221
 
1145
1222
 
1146
1223
  @app.command()
@@ -1159,16 +1236,16 @@ def uninit(
1159
1236
 
1160
1237
  def _confirm_uninit(preview_rep: InstallerReport) -> bool:
1161
1238
  if not json_output:
1162
- typer.echo("Remove CodeGraph project index files?\n")
1239
+ cli_echo("Remove CodeGraph project index files?\n")
1163
1240
  for c in preview_rep.changes:
1164
- typer.echo(f" - {c.display_path}")
1165
- typer.echo("\nContinue? [y/N] ", nl=False)
1241
+ cli_echo(f" - {c.display_path}")
1242
+ cli_echo("\nContinue? [y/N] ", nl=False)
1166
1243
  try:
1167
1244
  ans = input().strip().lower()
1168
1245
  except EOFError:
1169
1246
  ans = ""
1170
1247
  if not json_output:
1171
- typer.echo("")
1248
+ cli_echo("")
1172
1249
  return ans in ("y", "yes")
1173
1250
 
1174
1251
  try:
@@ -1179,15 +1256,15 @@ def uninit(
1179
1256
  )
1180
1257
  except CodeGraphError as exc:
1181
1258
  if json_output:
1182
- typer.echo(json.dumps(exc.to_response(), indent=2))
1259
+ cli_echo(json.dumps(exc.to_response(), indent=2), json_mode=True)
1183
1260
  else:
1184
- typer.echo(f"Error [{exc.code}]: {exc.message}")
1261
+ cli_echo(f"Error [{exc.code}]: {exc.message}")
1185
1262
  raise typer.Exit(code=1) from exc
1186
1263
 
1187
1264
  if json_output:
1188
- typer.echo(json.dumps(report.as_dict(), indent=2))
1265
+ cli_echo(json.dumps(report.as_dict(), indent=2), json_mode=True)
1189
1266
  else:
1190
- typer.echo(report.format_human())
1267
+ cli_echo(report.format_human())
1191
1268
 
1192
1269
 
1193
1270
  if __name__ == "__main__":
@@ -0,0 +1,190 @@
1
+ """Centralized cross-platform CLI output utility for Windows/macOS/Linux encoding safety.
2
+
3
+ Guarantees:
4
+ - Never assumes stdout/stderr is UTF-8 (safe on Windows cp1252, cp437, ASCII, and redirected pipes).
5
+ - Preserves rich Unicode symbols (`✓`, `✗`, `⚠`, `↻`, `(•)`) when the target stream supports them.
6
+ - Automatically degrades to deterministic ASCII markers (`[OK]`, `[ERROR]`, `[WARN]`, `[REPAIRED]`, `(*)`)
7
+ when the stream encoding does not support Unicode box/status glyphs.
8
+ - Ensures `--json` output is strictly machine-readable and never contains decorative Unicode glyphs.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import codecs
13
+ import os
14
+ import sys
15
+ from dataclasses import dataclass
16
+ from typing import Any, TextIO
17
+
18
+ import typer
19
+
20
+ _PROBE_GLYPHS = "✓✗⚠↻•→↓─—–"
21
+
22
+ # Ordered replacements so multi-char patterns (e.g. "(•)") are replaced before single chars ("•")
23
+ _UNICODE_TO_ASCII_MAP: tuple[tuple[str, str], ...] = (
24
+ ("(•)", "(*)"),
25
+ ("✓", "[OK]"),
26
+ ("✗", "[ERROR]"),
27
+ ("⚠", "[WARN]"),
28
+ ("↻", "[REPAIRED]"),
29
+ ("•", "*"),
30
+ ("→", "->"),
31
+ ("↓", "v"),
32
+ ("—", "--"),
33
+ ("–", "-"),
34
+ ("─", "-"),
35
+ )
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class CliSymbols:
40
+ """Encoding-aware CLI status symbols."""
41
+
42
+ unicode_enabled: bool
43
+ ok: str
44
+ error: str
45
+ warn: str
46
+ repair: str
47
+ radio_on: str
48
+ radio_off: str
49
+ arrow_right: str
50
+ arrow_down: str
51
+
52
+
53
+ UNICODE_SYMBOLS = CliSymbols(
54
+ unicode_enabled=True,
55
+ ok="✓",
56
+ error="✗",
57
+ warn="⚠",
58
+ repair="↻",
59
+ radio_on="(•)",
60
+ radio_off="( )",
61
+ arrow_right="→",
62
+ arrow_down="↓",
63
+ )
64
+
65
+ ASCII_SYMBOLS = CliSymbols(
66
+ unicode_enabled=False,
67
+ ok="[OK]",
68
+ error="[ERROR]",
69
+ warn="[WARN]",
70
+ repair="[REPAIRED]",
71
+ radio_on="(*)",
72
+ radio_off="( )",
73
+ arrow_right="->",
74
+ arrow_down="v",
75
+ )
76
+
77
+
78
+ def detect_stream_encoding(stream: Any = None) -> str:
79
+ """Safely detect the character encoding of `stream` (defaults to `sys.stdout`)."""
80
+ target = stream if stream is not None else sys.stdout
81
+ enc = getattr(target, "encoding", None)
82
+ if not enc or not isinstance(enc, str):
83
+ return "ascii"
84
+ try:
85
+ info = codecs.lookup(enc)
86
+ return info.name
87
+ except LookupError:
88
+ return "ascii"
89
+
90
+
91
+ def supports_unicode(
92
+ stream: Any = None,
93
+ *,
94
+ unicode_override: bool | None = None,
95
+ ) -> bool:
96
+ """Return True if `stream` can encode all CodeGraph Unicode status glyphs without error."""
97
+ if unicode_override is not None:
98
+ return unicode_override
99
+
100
+ env_ascii = os.environ.get("CODEGRAPH_ASCII_OUTPUT", "").strip().lower()
101
+ if env_ascii in ("1", "true", "yes", "on"):
102
+ return False
103
+
104
+ env_unicode = os.environ.get("CODEGRAPH_UNICODE_OUTPUT", "").strip().lower()
105
+ if env_unicode in ("1", "true", "yes", "on"):
106
+ return True
107
+ if env_unicode in ("0", "false", "no", "off"):
108
+ return False
109
+
110
+ enc = detect_stream_encoding(stream)
111
+ try:
112
+ _PROBE_GLYPHS.encode(enc, errors="strict")
113
+ return True
114
+ except (UnicodeEncodeError, LookupError):
115
+ return False
116
+
117
+
118
+ def get_cli_symbols(
119
+ stream: Any = None,
120
+ *,
121
+ unicode_override: bool | None = None,
122
+ ) -> CliSymbols:
123
+ """Return `UNICODE_SYMBOLS` when supported by `stream`, else `ASCII_SYMBOLS`."""
124
+ if supports_unicode(stream, unicode_override=unicode_override):
125
+ return UNICODE_SYMBOLS
126
+ return ASCII_SYMBOLS
127
+
128
+
129
+ def sanitize_text_for_stream(
130
+ text: str,
131
+ stream: Any = None,
132
+ *,
133
+ unicode_override: bool | None = None,
134
+ ) -> str:
135
+ """Format `text` safely for `stream`, converting decorative Unicode when unsupported."""
136
+ if not text:
137
+ return text
138
+
139
+ target = stream if stream is not None else sys.stdout
140
+ use_unicode = supports_unicode(target, unicode_override=unicode_override)
141
+ out = text
142
+ if not use_unicode:
143
+ for uni_tok, ascii_tok in _UNICODE_TO_ASCII_MAP:
144
+ if uni_tok in out:
145
+ out = out.replace(uni_tok, ascii_tok)
146
+
147
+ enc = detect_stream_encoding(target)
148
+ try:
149
+ out.encode(enc, errors="strict")
150
+ return out
151
+ except (UnicodeEncodeError, LookupError):
152
+ # Replace any remaining unencodable glyphs cleanly so write() never raises UnicodeEncodeError
153
+ for uni_tok, ascii_tok in _UNICODE_TO_ASCII_MAP:
154
+ if uni_tok in out:
155
+ out = out.replace(uni_tok, ascii_tok)
156
+ return out.encode(enc, errors="replace").decode(enc, errors="replace")
157
+
158
+
159
+ def sanitize_json_string(raw_json: str, stream: Any = None) -> str:
160
+ """Ensure machine-readable JSON output never crashes non-UTF-8 streams."""
161
+ enc = detect_stream_encoding(stream)
162
+ try:
163
+ raw_json.encode(enc, errors="strict")
164
+ return raw_json
165
+ except (UnicodeEncodeError, LookupError):
166
+ # Escape non-ASCII characters using standard JSON \uXXXX escapes
167
+ return raw_json.encode("unicode_escape").decode("ascii")
168
+
169
+
170
+ def cli_echo(
171
+ message: str = "",
172
+ *,
173
+ nl: bool = True,
174
+ err: bool = False,
175
+ json_mode: bool = False,
176
+ stream: TextIO | None = None,
177
+ unicode_override: bool | None = None,
178
+ ) -> None:
179
+ """Centralized safe CLI output function replacing raw `typer.echo`."""
180
+ target = stream if stream is not None else (sys.stderr if err else sys.stdout)
181
+ if json_mode:
182
+ safe_msg = sanitize_json_string(str(message), target)
183
+ else:
184
+ safe_msg = sanitize_text_for_stream(str(message), target, unicode_override=unicode_override)
185
+
186
+ if stream is not None:
187
+ stream.write(safe_msg + ("\n" if nl else ""))
188
+ stream.flush()
189
+ else:
190
+ typer.echo(safe_msg, nl=nl, err=err)
codegraph/context.py CHANGED
@@ -708,6 +708,9 @@ def _get_context_impl(
708
708
 
709
709
  stage1_candidate_count = len(candidate_dicts)
710
710
 
711
+ # Instantiate ConstraintGuard for hard safety invariants
712
+ _guard = ConstraintGuard.from_task_spec(task_spec)
713
+
711
714
  # Process framework routes
712
715
  route_relationships: list[Relationship] = []
713
716
  entry_points_out: list[dict[str, object]] = []
@@ -717,16 +720,26 @@ def _get_context_impl(
717
720
  r_path = r["route_path"]
718
721
  r_method = r["http_method"]
719
722
  ep_id = r["endpoint_id"] or f"{r_method} {r_path}"
723
+ r_segments = [
724
+ seg.lower()
725
+ for seg in str(r_path).split("/")
726
+ if len(seg) >= 4
727
+ and not seg.startswith("{")
728
+ and not seg.startswith("<")
729
+ and seg.lower() not in {"api", "v1", "v2"}
730
+ ]
720
731
  matches_task = (
721
732
  r_path in task_display_str
722
733
  or ep_id.lower() in task_display_str.lower()
723
734
  or (r_method in task_display_str.upper() and r_path in task_display_str)
735
+ or task_spec.intent == "ARCHITECTURE"
736
+ or any(seg in task_display_str.lower() for seg in r_segments)
724
737
  or any(
725
738
  (len(tgt) >= 3 and (tgt.lower() == r["handler_name"].lower() or tgt.lower() in r_path.lower()))
726
739
  for tgt in task_spec.targets
727
740
  )
728
741
  )
729
- if matches_task:
742
+ if matches_task and _guard.allows_symbol(str(r["handler_name"])) and _guard.allows_file(str(r["file_path"])):
730
743
  target_symbols.add(r["handler_name"])
731
744
  r_line = r["line"] or 1
732
745
  candidate_dicts.append(
@@ -799,8 +812,108 @@ def _get_context_impl(
799
812
  except Exception:
800
813
  pass
801
814
 
802
- # Instantiate ConstraintGuard for hard safety invariants
803
- _guard = ConstraintGuard.from_task_spec(task_spec)
815
+ # Seed architectural boundary classes and gateway methods for ARCHITECTURE intent
816
+ if task_spec.intent == "ARCHITECTURE":
817
+ try:
818
+ arch_rows = con.execute(
819
+ "SELECT canonical_id, qualified_name, name, kind, path, start_line, end_line "
820
+ "FROM symbols WHERE kind = 'class' OR name = 'app'"
821
+ ).fetchall()
822
+ for a_row in arch_rows:
823
+ a_name = str(a_row["name"])
824
+ a_sym = str(a_row["qualified_name"] or a_name)
825
+ a_cid = str(a_row["canonical_id"] or "")
826
+ a_file = str(a_row["path"] or "")
827
+ if (
828
+ _guard.allows_symbol(a_sym)
829
+ and _guard.allows_symbol(a_name)
830
+ and (not a_cid or _guard.allows_canonical_id(a_cid))
831
+ and _guard.allows_file(a_file)
832
+ ):
833
+ target_symbols.add(a_name)
834
+ target_symbols.add(a_sym)
835
+ candidate_dicts.append(
836
+ {
837
+ "file": a_file,
838
+ "symbol": a_name,
839
+ "canonical_id": a_cid,
840
+ "start_line": int(a_row["start_line"] or 1),
841
+ "end_line": int(a_row["end_line"] or 1),
842
+ "score": 0.90,
843
+ "relationship": "DEFINES",
844
+ "snippet": f"Architectural symbol {a_sym} ({a_row['kind']}) in {a_file}",
845
+ }
846
+ )
847
+ if "gateway" in task_display_str.lower():
848
+ gw_rows = con.execute(
849
+ "SELECT canonical_id, qualified_name, name, kind, path, start_line, end_line "
850
+ "FROM symbols WHERE qualified_name LIKE '%Gateway.%'"
851
+ ).fetchall()
852
+ for g_row in gw_rows:
853
+ g_name = str(g_row["name"])
854
+ g_sym = str(g_row["qualified_name"] or g_name)
855
+ g_cid = str(g_row["canonical_id"] or "")
856
+ g_file = str(g_row["path"] or "")
857
+ if (
858
+ _guard.allows_symbol(g_sym)
859
+ and _guard.allows_symbol(g_name)
860
+ and (not g_cid or _guard.allows_canonical_id(g_cid))
861
+ and _guard.allows_file(g_file)
862
+ ):
863
+ target_symbols.add(g_name)
864
+ target_symbols.add(g_sym)
865
+ candidate_dicts.append(
866
+ {
867
+ "file": g_file,
868
+ "symbol": g_name,
869
+ "canonical_id": g_cid,
870
+ "start_line": int(g_row["start_line"] or 1),
871
+ "end_line": int(g_row["end_line"] or 1),
872
+ "score": 0.88,
873
+ "relationship": "DEFINES",
874
+ "snippet": f"Gateway method {g_sym} in {g_file}",
875
+ }
876
+ )
877
+ except Exception:
878
+ pass
879
+
880
+ # Seed direct methods of resolved class targets
881
+ for res in _target_resolutions.values():
882
+ cls_qname = res.qualified_name
883
+ if cls_qname and res.target_type.value == "CLASS":
884
+ try:
885
+ m_rows = con.execute(
886
+ "SELECT canonical_id, qualified_name, name, kind, path, start_line, end_line "
887
+ "FROM symbols WHERE qualified_name LIKE ?",
888
+ (f"{cls_qname}.%",),
889
+ ).fetchall()
890
+ for m_row in m_rows:
891
+ m_name = str(m_row["name"])
892
+ m_sym = str(m_row["qualified_name"] or m_name)
893
+ m_cid = str(m_row["canonical_id"] or "")
894
+ m_file = str(m_row["path"] or "")
895
+ if (
896
+ _guard.allows_symbol(m_sym)
897
+ and _guard.allows_symbol(m_name)
898
+ and (not m_cid or _guard.allows_canonical_id(m_cid))
899
+ and _guard.allows_file(m_file)
900
+ ):
901
+ target_symbols.add(m_name)
902
+ target_symbols.add(m_sym)
903
+ candidate_dicts.append(
904
+ {
905
+ "file": m_file,
906
+ "symbol": m_name,
907
+ "canonical_id": m_cid,
908
+ "start_line": int(m_row["start_line"] or 1),
909
+ "end_line": int(m_row["end_line"] or 1),
910
+ "score": 0.88,
911
+ "relationship": "DEFINES",
912
+ "snippet": f"Method {m_sym} of target class {cls_qname}",
913
+ }
914
+ )
915
+ except Exception:
916
+ pass
804
917
 
805
918
  # 5. Graph exploration (callers/callees) & Tests with deterministic seeds
806
919
  t_graph_start = time.perf_counter()