codegraph-engine 3.0.0__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.
Files changed (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +868 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,205 @@
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
+ ("├── ", "|-- "),
26
+ ("└── ", "\\-- "),
27
+ ("├──", "|--"),
28
+ ("└──", "\\--"),
29
+ ("├─", "|-"),
30
+ ("└─", "\\-"),
31
+ ("│", "|"),
32
+ ("─", "-"),
33
+ ("✓", "[OK]"),
34
+ ("✗", "[ERROR]"),
35
+ ("⚠", "[WARN]"),
36
+ ("↻", "[REPAIRED]"),
37
+ ("⚠️", "[WARN]"),
38
+ ("✅", "[OK]"),
39
+ ("❌", "[ERROR]"),
40
+ ("📄", "[FILE]"),
41
+ ("📞", "[CALLER]"),
42
+ ("🌐", "[ROUTE]"),
43
+ ("🧪", "[TEST]"),
44
+ ("💾", "[DB]"),
45
+ ("•", "*"),
46
+ ("→", "->"),
47
+ ("↓", "v"),
48
+ ("—", "--"),
49
+ ("–", "-"),
50
+ )
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class CliSymbols:
55
+ """Encoding-aware CLI status symbols."""
56
+
57
+ unicode_enabled: bool
58
+ ok: str
59
+ error: str
60
+ warn: str
61
+ repair: str
62
+ radio_on: str
63
+ radio_off: str
64
+ arrow_right: str
65
+ arrow_down: str
66
+
67
+
68
+ UNICODE_SYMBOLS = CliSymbols(
69
+ unicode_enabled=True,
70
+ ok="✓",
71
+ error="✗",
72
+ warn="⚠",
73
+ repair="↻",
74
+ radio_on="(•)",
75
+ radio_off="( )",
76
+ arrow_right="→",
77
+ arrow_down="↓",
78
+ )
79
+
80
+ ASCII_SYMBOLS = CliSymbols(
81
+ unicode_enabled=False,
82
+ ok="[OK]",
83
+ error="[ERROR]",
84
+ warn="[WARN]",
85
+ repair="[REPAIRED]",
86
+ radio_on="(*)",
87
+ radio_off="( )",
88
+ arrow_right="->",
89
+ arrow_down="v",
90
+ )
91
+
92
+
93
+ def detect_stream_encoding(stream: Any = None) -> str:
94
+ """Safely detect the character encoding of `stream` (defaults to `sys.stdout`)."""
95
+ target = stream if stream is not None else sys.stdout
96
+ enc = getattr(target, "encoding", None)
97
+ if not enc or not isinstance(enc, str):
98
+ return "ascii"
99
+ try:
100
+ info = codecs.lookup(enc)
101
+ return info.name
102
+ except LookupError:
103
+ return "ascii"
104
+
105
+
106
+ def supports_unicode(
107
+ stream: Any = None,
108
+ *,
109
+ unicode_override: bool | None = None,
110
+ ) -> bool:
111
+ """Return True if `stream` can encode all CodeGraph Unicode status glyphs without error."""
112
+ if unicode_override is not None:
113
+ return unicode_override
114
+
115
+ env_ascii = os.environ.get("CODEGRAPH_ASCII_OUTPUT", "").strip().lower()
116
+ if env_ascii in ("1", "true", "yes", "on"):
117
+ return False
118
+
119
+ env_unicode = os.environ.get("CODEGRAPH_UNICODE_OUTPUT", "").strip().lower()
120
+ if env_unicode in ("1", "true", "yes", "on"):
121
+ return True
122
+ if env_unicode in ("0", "false", "no", "off"):
123
+ return False
124
+
125
+ enc = detect_stream_encoding(stream)
126
+ try:
127
+ _PROBE_GLYPHS.encode(enc, errors="strict")
128
+ return True
129
+ except (UnicodeEncodeError, LookupError):
130
+ return False
131
+
132
+
133
+ def get_cli_symbols(
134
+ stream: Any = None,
135
+ *,
136
+ unicode_override: bool | None = None,
137
+ ) -> CliSymbols:
138
+ """Return `UNICODE_SYMBOLS` when supported by `stream`, else `ASCII_SYMBOLS`."""
139
+ if supports_unicode(stream, unicode_override=unicode_override):
140
+ return UNICODE_SYMBOLS
141
+ return ASCII_SYMBOLS
142
+
143
+
144
+ def sanitize_text_for_stream(
145
+ text: str,
146
+ stream: Any = None,
147
+ *,
148
+ unicode_override: bool | None = None,
149
+ ) -> str:
150
+ """Format `text` safely for `stream`, converting decorative Unicode when unsupported."""
151
+ if not text:
152
+ return text
153
+
154
+ target = stream if stream is not None else sys.stdout
155
+ use_unicode = supports_unicode(target, unicode_override=unicode_override)
156
+ out = text
157
+ if not use_unicode:
158
+ for uni_tok, ascii_tok in _UNICODE_TO_ASCII_MAP:
159
+ if uni_tok in out:
160
+ out = out.replace(uni_tok, ascii_tok)
161
+
162
+ enc = detect_stream_encoding(target)
163
+ try:
164
+ out.encode(enc, errors="strict")
165
+ return out
166
+ except (UnicodeEncodeError, LookupError):
167
+ # Replace any remaining unencodable glyphs cleanly so write() never raises UnicodeEncodeError
168
+ for uni_tok, ascii_tok in _UNICODE_TO_ASCII_MAP:
169
+ if uni_tok in out:
170
+ out = out.replace(uni_tok, ascii_tok)
171
+ return out.encode(enc, errors="replace").decode(enc, errors="replace")
172
+
173
+
174
+ def sanitize_json_string(raw_json: str, stream: Any = None) -> str:
175
+ """Ensure machine-readable JSON output never crashes non-UTF-8 streams."""
176
+ enc = detect_stream_encoding(stream)
177
+ try:
178
+ raw_json.encode(enc, errors="strict")
179
+ return raw_json
180
+ except (UnicodeEncodeError, LookupError):
181
+ # Escape non-ASCII characters using standard JSON \uXXXX escapes
182
+ return raw_json.encode("unicode_escape").decode("ascii")
183
+
184
+
185
+ def cli_echo(
186
+ message: str = "",
187
+ *,
188
+ nl: bool = True,
189
+ err: bool = False,
190
+ json_mode: bool = False,
191
+ stream: TextIO | None = None,
192
+ unicode_override: bool | None = None,
193
+ ) -> None:
194
+ """Centralized safe CLI output function replacing raw `typer.echo`."""
195
+ target = stream if stream is not None else (sys.stderr if err else sys.stdout)
196
+ if json_mode:
197
+ safe_msg = sanitize_json_string(str(message), target)
198
+ else:
199
+ safe_msg = sanitize_text_for_stream(str(message), target, unicode_override=unicode_override)
200
+
201
+ if stream is not None:
202
+ stream.write(safe_msg + ("\n" if nl else ""))
203
+ stream.flush()
204
+ else:
205
+ typer.echo(safe_msg, nl=nl, err=err)
codegraph/config.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class Settings(BaseModel):
10
+ """Runtime settings. Environment values deliberately never include API keys."""
11
+
12
+ repository: Path | None = None
13
+ db_path: Path | None = None
14
+ max_file_size: int = Field(default=1_000_000, ge=1)
15
+ max_read_bytes: int = Field(default=256_000, ge=1024)
16
+ max_results: int = Field(default=10, ge=1, le=100)
17
+ max_context: int = Field(default=20_000, ge=100)
18
+ max_tool_calls: int = Field(default=12, ge=1)
19
+ exclude: tuple[str, ...] = ()
20
+ resource_profile: str = "BALANCED"
21
+ max_interactive_workers: int = Field(default=2, ge=1)
22
+ max_background_workers: int = Field(default=1, ge=0)
23
+ max_memory_mb: int = Field(default=1024, ge=128)
24
+ max_graph_nodes: int = Field(default=300, ge=10)
25
+ max_graph_edges: int = Field(default=1200, ge=10)
26
+ max_git_commits: int = Field(default=15, ge=1)
27
+ background_indexing: bool = True
28
+ debounce_ms: int = Field(default=500, ge=50)
29
+
30
+ @classmethod
31
+ def from_env(cls) -> Settings:
32
+ repo = os.getenv("CODEGRAPH_REPOSITORY")
33
+ db = os.getenv("CODEGRAPH_DB_PATH")
34
+ return cls(
35
+ repository=Path(repo) if repo else None,
36
+ db_path=Path(db) if db else None,
37
+ max_file_size=int(os.getenv("CODEGRAPH_MAX_FILE_SIZE", "1000000")),
38
+ max_read_bytes=int(os.getenv("CODEGRAPH_MAX_READ_BYTES", "256000")),
39
+ max_context=int(os.getenv("CODEGRAPH_MAX_CONTEXT", "20000")),
40
+ max_tool_calls=int(os.getenv("CODEGRAPH_MAX_TOOL_CALLS", "12")),
41
+ resource_profile=os.getenv("CODEGRAPH_RESOURCE_PROFILE", "BALANCED"),
42
+ max_memory_mb=int(os.getenv("CODEGRAPH_MAX_MEMORY_MB", "1024")),
43
+ background_indexing=os.getenv("CODEGRAPH_BACKGROUND_INDEXING", "true").lower() in ("true", "1"),
44
+ debounce_ms=int(os.getenv("CODEGRAPH_DEBOUNCE_MS", "500")),
45
+ )
@@ -0,0 +1,238 @@
1
+ """Constraint Guard for CodeGraph MCP v2.1.
2
+
3
+ Enforces deterministic hard constraints across every retrieval and compilation stage:
4
+ - Explicit excluded symbols, hierarchical descendants, and canonical IDs
5
+ - Explicit excluded files, directories, and path-component matching
6
+ - Excluded modules and namespaces
7
+ - Scope paths and scope modules
8
+ - Repository and security boundary protection
9
+
10
+ Architectural Invariant:
11
+ Hard constraints are HARD FILTERS, never ranking penalties.
12
+ No entity violating a hard constraint may participate in candidate generation,
13
+ graph traversal, ranking, coverage, or appear in the final ContextPacket.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+ from pathlib import PurePosixPath
19
+ from typing import Any
20
+
21
+
22
+ def _normalize_path(p: str | None) -> str:
23
+ if not p:
24
+ return ""
25
+ norm = p.replace("\\", "/").strip()
26
+ while norm.startswith("./"):
27
+ norm = norm[2:]
28
+ return norm.strip("/")
29
+
30
+
31
+ def _is_hierarchical_symbol_match(sym: str, excl: str) -> bool:
32
+ """Return True if `sym` matches `excl` or is a hierarchical descendant of `excl`.
33
+
34
+ Examples:
35
+ - excl="AuthService", sym="AuthService" -> True
36
+ - excl="AuthService", sym="AuthService.login" -> True
37
+ - excl="AuthService", sym="auth.AuthService.login" -> True
38
+ - excl="AuthService", sym="AuthServiceHelper" -> False (distinct symbol)
39
+ - excl="AuthService", sym="UserAuthService" -> False (distinct symbol)
40
+ """
41
+ if not sym or not excl:
42
+ return False
43
+ if sym == excl:
44
+ return True
45
+ if sym.startswith(f"{excl}.") or sym.startswith(f"{excl}:"):
46
+ return True
47
+
48
+ # Check dot-separated hierarchy components
49
+ parts = sym.split(".")
50
+ for part in parts:
51
+ if part == excl:
52
+ return True
53
+
54
+ # Check colon-separated hierarchy components (e.g. file:Class.method)
55
+ col_parts = sym.split(":")
56
+ for part in col_parts:
57
+ if part == excl or part.startswith(f"{excl}."):
58
+ return True
59
+ return False
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ConstraintGuard:
64
+ """Central hard-constraint enforcement guard."""
65
+
66
+ excluded_symbols: frozenset[str] = field(default_factory=frozenset)
67
+ excluded_files: frozenset[str] = field(default_factory=frozenset)
68
+ excluded_modules: frozenset[str] = field(default_factory=frozenset)
69
+ scope_paths: frozenset[str] = field(default_factory=frozenset)
70
+ scope_modules: frozenset[str] = field(default_factory=frozenset)
71
+
72
+ @classmethod
73
+ def from_task_spec(cls, task_spec: Any) -> ConstraintGuard:
74
+ """Construct a ConstraintGuard from a TaskSpec or dictionary."""
75
+ if not task_spec:
76
+ return cls()
77
+
78
+ excl_syms: set[str] = set()
79
+ excl_files: set[str] = set()
80
+ excl_mods: set[str] = set()
81
+
82
+ raw_exclusions = getattr(task_spec, "exclusions", ()) or ()
83
+ if isinstance(raw_exclusions, (list, tuple, set)):
84
+ for item in raw_exclusions:
85
+ if not item:
86
+ continue
87
+ s = str(item).strip()
88
+ if "/" in s or "\\" in s or s.endswith(".py") or s.endswith(".ts") or s.endswith(".js"):
89
+ excl_files.add(_normalize_path(s))
90
+ else:
91
+ excl_syms.add(s)
92
+
93
+ raw_scope_paths = getattr(task_spec, "scope_paths", ()) or ()
94
+ scope_p = {_normalize_path(str(p)) for p in raw_scope_paths if p}
95
+
96
+ raw_scope_mods = getattr(task_spec, "scope_modules", ()) or ()
97
+ scope_m = {str(m).strip() for m in raw_scope_mods if m}
98
+
99
+ return cls(
100
+ excluded_symbols=frozenset(excl_syms),
101
+ excluded_files=frozenset(excl_files),
102
+ excluded_modules=frozenset(excl_mods),
103
+ scope_paths=frozenset(scope_p),
104
+ scope_modules=frozenset(scope_m),
105
+ )
106
+
107
+ def allows_symbol(self, symbol: str | None) -> bool:
108
+ """Check if symbol is permitted under exclusion and scope constraints."""
109
+ if not symbol:
110
+ return True
111
+ s = symbol.strip()
112
+ for excl in self.excluded_symbols:
113
+ if _is_hierarchical_symbol_match(s, excl):
114
+ return False
115
+ return True
116
+
117
+ def allows_canonical_id(self, canonical_id: str | None) -> bool:
118
+ """Check if canonical symbol ID is permitted."""
119
+ if not canonical_id:
120
+ return True
121
+ cid = canonical_id.strip()
122
+ if not self.allows_symbol(cid):
123
+ return False
124
+ # If canonical_id encodes a file path, verify file is allowed
125
+ if ":" in cid:
126
+ path_part = cid.split(":", 1)[0]
127
+ if "/" in path_part or path_part.endswith((".py", ".ts", ".js")):
128
+ if not self.allows_file(path_part):
129
+ return False
130
+ return True
131
+
132
+ def allows_file(self, file_path: str | None) -> bool:
133
+ """Check if file path is permitted under exclusion, path-component, and scope constraints."""
134
+ if not file_path:
135
+ return True
136
+ path_norm = _normalize_path(file_path)
137
+
138
+ # 1. Security check: reject path traversal
139
+ path_obj = PurePosixPath(path_norm)
140
+ if ".." in path_obj.parts:
141
+ return False
142
+
143
+ # 2. Hard exclusions
144
+ for excl in self.excluded_files:
145
+ excl_norm = _normalize_path(excl)
146
+ if not excl_norm:
147
+ continue
148
+ # Exact path match
149
+ if path_norm == excl_norm:
150
+ return False
151
+ # Directory prefix match (e.g. excl="src/admin_panel", path="src/admin_panel/views.py")
152
+ if path_norm.startswith(f"{excl_norm}/"):
153
+ return False
154
+ # Suffix / component match if exclusion is just a filename
155
+ if "/" not in excl_norm and path_obj.name == excl_norm:
156
+ return False
157
+
158
+ # 3. Scope constraints (if scope paths specified, path must match at least one)
159
+ if self.scope_paths:
160
+ in_scope = False
161
+ for sp in self.scope_paths:
162
+ if path_norm == sp or path_norm.startswith(f"{sp}/"):
163
+ in_scope = True
164
+ break
165
+ if not in_scope:
166
+ return False
167
+
168
+ return True
169
+
170
+ def allows_module(self, module_name: str | None) -> bool:
171
+ """Check if module is permitted."""
172
+ if not module_name:
173
+ return True
174
+ m = module_name.strip()
175
+ for excl in self.excluded_modules:
176
+ if m == excl or m.startswith(f"{excl}."):
177
+ return False
178
+ if self.scope_modules:
179
+ in_scope = False
180
+ for sm in self.scope_modules:
181
+ if m == sm or m.startswith(f"{sm}."):
182
+ in_scope = True
183
+ break
184
+ if not in_scope:
185
+ return False
186
+ return True
187
+
188
+ def allows_relationship(
189
+ self,
190
+ source_id: str | None,
191
+ rel_type: str | None,
192
+ target_id: str | None,
193
+ ) -> bool:
194
+ """Check if relationship endpoints are permitted."""
195
+ if source_id and not self.allows_canonical_id(source_id) and not self.allows_symbol(source_id):
196
+ return False
197
+ if target_id and not self.allows_canonical_id(target_id) and not self.allows_symbol(target_id):
198
+ return False
199
+ return True
200
+
201
+ def allows_test(
202
+ self,
203
+ test_file: str | None,
204
+ test_symbol: str | None = None,
205
+ ) -> bool:
206
+ """Check if test artifact is permitted."""
207
+ if test_file and not self.allows_file(test_file):
208
+ return False
209
+ if test_symbol and not self.allows_symbol(test_symbol):
210
+ return False
211
+ return True
212
+
213
+ def allows_endpoint(
214
+ self,
215
+ route_path: str | None,
216
+ handler_name: str | None = None,
217
+ file_path: str | None = None,
218
+ ) -> bool:
219
+ """Check if framework route is permitted."""
220
+ if file_path and not self.allows_file(file_path):
221
+ return False
222
+ if handler_name and not self.allows_symbol(handler_name):
223
+ return False
224
+ if route_path and route_path in self.excluded_symbols:
225
+ return False
226
+ return True
227
+
228
+ def allows_evidence(
229
+ self,
230
+ file_path: str | None,
231
+ symbol: str | None = None,
232
+ ) -> bool:
233
+ """Check if evidence item is permitted."""
234
+ if file_path and not self.allows_file(file_path):
235
+ return False
236
+ if symbol and not self.allows_symbol(symbol):
237
+ return False
238
+ return True