Skip to content

Commit 9c49215

Browse files
pwx603claude
andcommitted
feat: ccarch resolver with ts/tsx recovery and status codes
Resolves doc-cited code paths against the live filesystem, reporting ok/moved/missing. Never raises on junk input (empty strings, path traversal attempts, nonexistent paths); a cited path can never resolve outside the repo root. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 901489b commit 9c49215

2 files changed

Lines changed: 92 additions & 0 deletions

File tree

mcp/ccarch/ccarch/resolver.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Resolve doc-cited code paths against the live filesystem.
2+
3+
Resolution happens per call, not at index time: docs and code drift
4+
independently, so the 5% dead rate is permanent, not a one-off cleanup.
5+
Status is a first-class field — an agent told `missing` adapts, while an
6+
agent handed a phantom path burns a turn discovering it.
7+
"""
8+
from __future__ import annotations
9+
10+
from dataclasses import dataclass
11+
from pathlib import Path
12+
13+
14+
@dataclass
15+
class Resolved:
16+
path: str
17+
status: str # "ok" | "moved" | "missing"
18+
kind: str # "file" | "dir"
19+
20+
21+
def _swap_ext(p: str) -> str | None:
22+
if p.endswith(".ts"):
23+
return p[:-3] + ".tsx"
24+
if p.endswith(".tsx"):
25+
return p[:-4] + ".ts"
26+
return None
27+
28+
29+
def resolve(repo_root: Path, cited: str) -> Resolved:
30+
repo_root = Path(repo_root)
31+
raw = (cited or "").strip()
32+
if not raw:
33+
return Resolved(path=cited or "", status="missing", kind="file")
34+
35+
is_dir_cite = raw.endswith("/")
36+
q = raw.rstrip("/")
37+
38+
# Never let a cited path escape the repo. A doc citing ../../etc/passwd
39+
# resolves to missing rather than probing outside the tree.
40+
try:
41+
target = (repo_root / q).resolve()
42+
target.relative_to(repo_root.resolve())
43+
except (ValueError, OSError):
44+
return Resolved(path=raw, status="missing", kind="dir" if is_dir_cite else "file")
45+
46+
if is_dir_cite:
47+
return Resolved(
48+
path=raw,
49+
status="ok" if target.is_dir() else "missing",
50+
kind="dir",
51+
)
52+
53+
if target.is_file():
54+
return Resolved(path=q, status="ok", kind="file")
55+
if target.is_dir():
56+
return Resolved(path=q, status="ok", kind="dir")
57+
58+
alt = _swap_ext(q)
59+
if alt and (repo_root / alt).is_file():
60+
return Resolved(path=alt, status="moved", kind="file")
61+
62+
return Resolved(path=q, status="missing", kind="file")

mcp/ccarch/tests/test_resolver.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from pathlib import Path
2+
from ccarch.resolver import resolve
3+
4+
REPO = Path(__file__).resolve().parents[3]
5+
6+
def test_exact_hit_is_ok():
7+
r = resolve(REPO, "src/voice/voiceModeEnabled.ts")
8+
assert r.status == "ok"
9+
assert r.kind == "file"
10+
assert r.path == "src/voice/voiceModeEnabled.ts"
11+
12+
def test_wrong_extension_is_moved_with_corrected_path():
13+
# Docs cite AgentTool.ts; the real file is AgentTool.tsx.
14+
r = resolve(REPO, "packages/builtin-tools/src/tools/AgentTool/AgentTool.ts")
15+
assert r.status == "moved"
16+
assert r.path.endswith(".tsx")
17+
assert (REPO / r.path).is_file()
18+
19+
def test_directory_row_is_ok_kind_dir():
20+
r = resolve(REPO, "src/voice/")
21+
assert r.status == "ok"
22+
assert r.kind == "dir"
23+
24+
def test_genuinely_missing_is_missing():
25+
r = resolve(REPO, "src/definitely/not/here.ts")
26+
assert r.status == "missing"
27+
28+
def test_never_raises_on_junk():
29+
for junk in ["", " ", "///", "src/foo.ts", "../../etc/passwd"]:
30+
assert resolve(REPO, junk).status in {"ok", "moved", "missing"}

0 commit comments

Comments
 (0)