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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +868 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
codegraph/cli_output.py
ADDED
|
@@ -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
|
+
)
|
codegraph/constraints.py
ADDED
|
@@ -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
|