Clean up nested duplicate: v2/v2/skills/code_analysis.py
Browse files- v2/v2/skills/code_analysis.py +0 -138
v2/v2/skills/code_analysis.py
DELETED
|
@@ -1,138 +0,0 @@
|
|
| 1 |
-
"""Code analysis chip — reads actual campus source before anyone opines.
|
| 2 |
-
|
| 3 |
-
Extracts file paths and symbols from the request, reads those files from
|
| 4 |
-
the repo (when present), follows one level of imports, and grabs recent
|
| 5 |
-
git history for each file. Everything read is recorded in the session's
|
| 6 |
-
evidence log — this is what lets the discipline gate honestly say HIGH
|
| 7 |
-
confidence ("I read the file") vs LOW ("I'm reasoning from the map").
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
import re
|
| 11 |
-
|
| 12 |
-
from kintsugi_core import (
|
| 13 |
-
BaseSkillChip,
|
| 14 |
-
EFEWeights,
|
| 15 |
-
SkillCapability,
|
| 16 |
-
SkillContext,
|
| 17 |
-
SkillDomain,
|
| 18 |
-
SkillRequest,
|
| 19 |
-
SkillResponse,
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
PATH_RE = re.compile(
|
| 23 |
-
r"\b((?:server|client|shared|design-system)/[\w./-]+\.(?:ts|tsx|js|jsx|sql|json))\b"
|
| 24 |
-
)
|
| 25 |
-
SYMBOL_RE = re.compile(r"\b([a-z][a-zA-Z0-9]+(?:Handler|Store|Service|Middleware|Router))\b")
|
| 26 |
-
IMPORT_RE = re.compile(r"""from\s+['"]([^'"]+)['"]""")
|
| 27 |
-
|
| 28 |
-
MAX_FILES = 6
|
| 29 |
-
MAX_EXCERPT = 4000
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
class CodeAnalysisChip(BaseSkillChip):
|
| 33 |
-
name = "code_analysis"
|
| 34 |
-
description = "Read and trace campus source files named in the request"
|
| 35 |
-
version = "2.0.0"
|
| 36 |
-
domain = SkillDomain.RESEARCH
|
| 37 |
-
efe_weights = EFEWeights(
|
| 38 |
-
mission_alignment=0.20, stakeholder_benefit=0.20,
|
| 39 |
-
resource_efficiency=0.25, transparency=0.25, equity=0.10,
|
| 40 |
-
)
|
| 41 |
-
capabilities = [SkillCapability.READ_DATA]
|
| 42 |
-
|
| 43 |
-
def __init__(self, repo_files=None, git_tools=None):
|
| 44 |
-
super().__init__()
|
| 45 |
-
self.repo_files = repo_files
|
| 46 |
-
self.git_tools = git_tools
|
| 47 |
-
|
| 48 |
-
async def handle(self, request: SkillRequest,
|
| 49 |
-
context: SkillContext) -> SkillResponse:
|
| 50 |
-
question = context.metadata.get("question", request.raw_input)
|
| 51 |
-
session = context.metadata.get("session")
|
| 52 |
-
|
| 53 |
-
paths = list(dict.fromkeys(PATH_RE.findall(question)))[:MAX_FILES]
|
| 54 |
-
symbols = list(dict.fromkeys(SYMBOL_RE.findall(question)))[:5]
|
| 55 |
-
|
| 56 |
-
analysis = {
|
| 57 |
-
"repo_available": self.repo_files is not None,
|
| 58 |
-
"requested_paths": paths,
|
| 59 |
-
"symbols": symbols,
|
| 60 |
-
"files": [], # [{path, excerpt, git_status, recent_history, imports}]
|
| 61 |
-
"symbol_sites": [],
|
| 62 |
-
"notes": [],
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
if self.repo_files is None:
|
| 66 |
-
analysis["notes"].append(
|
| 67 |
-
"Campus repo is not on this machine — analysis is limited "
|
| 68 |
-
"to the architecture map. Confidence will be capped."
|
| 69 |
-
)
|
| 70 |
-
return self._done(analysis, session)
|
| 71 |
-
|
| 72 |
-
# Locate files for bare symbols the user named.
|
| 73 |
-
for sym in symbols:
|
| 74 |
-
hits = self.repo_files.search(rf"\b{re.escape(sym)}\b")[:3]
|
| 75 |
-
analysis["symbol_sites"].extend(hits)
|
| 76 |
-
for h in hits:
|
| 77 |
-
if h["path"] not in paths and len(paths) < MAX_FILES:
|
| 78 |
-
paths.append(h["path"])
|
| 79 |
-
|
| 80 |
-
for path in paths[:MAX_FILES]:
|
| 81 |
-
read = self.repo_files.read(path)
|
| 82 |
-
if not read.ok:
|
| 83 |
-
analysis["notes"].append(f"could not read {path}: {read.error}")
|
| 84 |
-
continue
|
| 85 |
-
entry = {
|
| 86 |
-
"path": path,
|
| 87 |
-
"excerpt": read.content[:MAX_EXCERPT],
|
| 88 |
-
"truncated": read.truncated or len(read.content) > MAX_EXCERPT,
|
| 89 |
-
"git_status": read.git_status,
|
| 90 |
-
"imports": IMPORT_RE.findall(read.content)[:20],
|
| 91 |
-
"recent_history": (
|
| 92 |
-
self.git_tools.recent_authors(path) if self.git_tools else ""
|
| 93 |
-
),
|
| 94 |
-
}
|
| 95 |
-
analysis["files"].append(entry)
|
| 96 |
-
if session:
|
| 97 |
-
session.record_file_read(path, self.name)
|
| 98 |
-
|
| 99 |
-
# One level of local-import tracing for the first file.
|
| 100 |
-
if analysis["files"]:
|
| 101 |
-
first = analysis["files"][0]
|
| 102 |
-
local = [imp for imp in first["imports"] if imp.startswith(".")][:3]
|
| 103 |
-
for imp in local:
|
| 104 |
-
resolved = self._resolve_import(first["path"], imp)
|
| 105 |
-
if resolved and len(analysis["files"]) < MAX_FILES:
|
| 106 |
-
read = self.repo_files.read(resolved)
|
| 107 |
-
if read.ok:
|
| 108 |
-
analysis["files"].append({
|
| 109 |
-
"path": resolved,
|
| 110 |
-
"excerpt": read.content[:MAX_EXCERPT // 2],
|
| 111 |
-
"truncated": True,
|
| 112 |
-
"git_status": read.git_status,
|
| 113 |
-
"imports": [],
|
| 114 |
-
"recent_history": "",
|
| 115 |
-
"traced_from": first["path"],
|
| 116 |
-
})
|
| 117 |
-
if session:
|
| 118 |
-
session.record_file_read(resolved, self.name)
|
| 119 |
-
|
| 120 |
-
return self._done(analysis, session)
|
| 121 |
-
|
| 122 |
-
def _resolve_import(self, from_path: str, import_spec: str) -> str:
|
| 123 |
-
base = "/".join(from_path.split("/")[:-1])
|
| 124 |
-
candidate = f"{base}/{import_spec.lstrip('./')}"
|
| 125 |
-
for suffix in (".ts", ".tsx", "/index.ts", ".js"):
|
| 126 |
-
probe = candidate + suffix
|
| 127 |
-
if self.repo_files.read(probe).ok:
|
| 128 |
-
return probe
|
| 129 |
-
return ""
|
| 130 |
-
|
| 131 |
-
def _done(self, analysis: dict, session) -> SkillResponse:
|
| 132 |
-
n_files = len(analysis["files"])
|
| 133 |
-
summary = (
|
| 134 |
-
f"read {n_files} file(s), "
|
| 135 |
-
f"{len(analysis['symbol_sites'])} symbol site(s); "
|
| 136 |
-
f"repo_available={analysis['repo_available']}"
|
| 137 |
-
)
|
| 138 |
-
return SkillResponse(content=summary, success=True, data=analysis)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|