sourcecode 0.25.0__py3-none-any.whl → 0.26.0__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.
- sourcecode/__init__.py +1 -1
- sourcecode/architecture_summary.py +40 -2
- sourcecode/cli.py +8 -0
- sourcecode/detectors/nodejs.py +131 -41
- sourcecode/graph_analyzer.py +50 -1
- sourcecode/prepare_context.py +73 -4
- sourcecode/relevance_scorer.py +182 -0
- sourcecode/runtime_classifier.py +331 -0
- sourcecode/schema.py +35 -2
- sourcecode/serializer.py +32 -2
- sourcecode/summarizer.py +75 -1
- {sourcecode-0.25.0.dist-info → sourcecode-0.26.0.dist-info}/METADATA +1 -1
- {sourcecode-0.25.0.dist-info → sourcecode-0.26.0.dist-info}/RECORD +15 -13
- {sourcecode-0.25.0.dist-info → sourcecode-0.26.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.25.0.dist-info → sourcecode-0.26.0.dist-info}/entry_points.txt +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -9,6 +9,12 @@ from sourcecode.schema import EntryPoint, SourceMap, StackDetection
|
|
|
9
9
|
from sourcecode.tree_utils import flatten_file_tree
|
|
10
10
|
|
|
11
11
|
_TOOLING_PREFIXES = (".claude/", ".vscode/", "bin/")
|
|
12
|
+
_AUXILIARY_PATH_PARTS: frozenset[str] = frozenset({
|
|
13
|
+
"benchmark", "benchmarks", "bench", "benches",
|
|
14
|
+
"demo", "demos", "example", "examples",
|
|
15
|
+
"docs", "doc", "fixture", "fixtures",
|
|
16
|
+
"playground", "playgrounds", "sandbox",
|
|
17
|
+
})
|
|
12
18
|
_PYTHON_EXTENSIONS = {".py"}
|
|
13
19
|
_NODE_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
|
|
14
20
|
_GO_EXTENSIONS = {".go"}
|
|
@@ -52,10 +58,12 @@ class ArchitectureSummarizer:
|
|
|
52
58
|
# Rich path: use all available signals (stacks, arch analysis, graph)
|
|
53
59
|
rich_lines = self._build_rich_lines(sm)
|
|
54
60
|
|
|
55
|
-
# Stack-specific entry point analysis
|
|
61
|
+
# Stack-specific entry point analysis: exclude tooling + auxiliary paths
|
|
56
62
|
entry_points = [
|
|
57
63
|
entry for entry in sm.entry_points
|
|
58
64
|
if not self._is_tooling_path(entry.path)
|
|
65
|
+
and not self._is_auxiliary_path(entry.path)
|
|
66
|
+
and entry.entrypoint_type not in ("benchmark", "example")
|
|
59
67
|
]
|
|
60
68
|
if not entry_points:
|
|
61
69
|
fallback = self._infer_fallback_entry_points(file_paths, sm.stacks)
|
|
@@ -147,7 +155,31 @@ class ArchitectureSummarizer:
|
|
|
147
155
|
if primary is None:
|
|
148
156
|
return ""
|
|
149
157
|
stack_label = _STACK_LABELS.get(primary.stack, primary.stack)
|
|
150
|
-
|
|
158
|
+
|
|
159
|
+
# For monorepos: only show frameworks from runtime-critical packages,
|
|
160
|
+
# not every framework found in every workspace (avoids "React project" misclassification)
|
|
161
|
+
if sm.project_type == "monorepo" and sm.monorepo_packages:
|
|
162
|
+
runtime_roles = {"runtime_core", "plugin_host", "backend_runtime"}
|
|
163
|
+
runtime_paths = {p.path for p in sm.monorepo_packages if p.architectural_role in runtime_roles}
|
|
164
|
+
fw_names = [
|
|
165
|
+
f.name
|
|
166
|
+
for s in sm.stacks
|
|
167
|
+
if not s.workspace or s.workspace in runtime_paths
|
|
168
|
+
for f in s.frameworks[:2]
|
|
169
|
+
]
|
|
170
|
+
fw_names = list(dict.fromkeys(fw_names))[:3] # dedup, preserve order
|
|
171
|
+
else:
|
|
172
|
+
# Filter out frameworks from auxiliary or tooling stacks (docs/, benchmarks/, etc.)
|
|
173
|
+
fw_names = [
|
|
174
|
+
f.name
|
|
175
|
+
for s in sm.stacks
|
|
176
|
+
if not self._is_tooling_path(s.root or "")
|
|
177
|
+
and not self._is_tooling_path(s.workspace or "")
|
|
178
|
+
and not self._is_auxiliary_path(s.root or "")
|
|
179
|
+
and not self._is_auxiliary_path(s.workspace or "")
|
|
180
|
+
for f in s.frameworks[:2]
|
|
181
|
+
][:3]
|
|
182
|
+
|
|
151
183
|
fw_str = f" using {', '.join(fw_names)}" if fw_names else ""
|
|
152
184
|
if runtime:
|
|
153
185
|
return f"{stack_label} {runtime.lower()}{fw_str}."
|
|
@@ -443,6 +475,12 @@ class ArchitectureSummarizer:
|
|
|
443
475
|
except OSError:
|
|
444
476
|
return None
|
|
445
477
|
|
|
478
|
+
def _is_auxiliary_path(self, path: str | None) -> bool:
|
|
479
|
+
if not path:
|
|
480
|
+
return False
|
|
481
|
+
parts = path.replace("\\", "/").strip("/").lower().split("/")
|
|
482
|
+
return any(p in _AUXILIARY_PATH_PARTS for p in parts)
|
|
483
|
+
|
|
446
484
|
def _is_tooling_path(self, path: str | None) -> bool:
|
|
447
485
|
if not path:
|
|
448
486
|
return False
|
sourcecode/cli.py
CHANGED
|
@@ -555,6 +555,14 @@ def main(
|
|
|
555
555
|
semantic_summary=sem_sum,
|
|
556
556
|
)
|
|
557
557
|
|
|
558
|
+
# Runtime architecture — classify workspace packages for structural summaries
|
|
559
|
+
if workspace_analysis.workspaces:
|
|
560
|
+
from sourcecode.runtime_classifier import RuntimeClassifier
|
|
561
|
+
sm.monorepo_packages = RuntimeClassifier().classify(
|
|
562
|
+
target,
|
|
563
|
+
[ws.path for ws in workspace_analysis.workspaces],
|
|
564
|
+
)
|
|
565
|
+
|
|
558
566
|
# Phase 9: LLM Output Quality — poblar campos derivados
|
|
559
567
|
from sourcecode.architecture_summary import ArchitectureSummarizer
|
|
560
568
|
from sourcecode.summarizer import ProjectSummarizer
|
sourcecode/detectors/nodejs.py
CHANGED
|
@@ -117,59 +117,149 @@ class NodejsDetector(AbstractDetector):
|
|
|
117
117
|
return package_manager
|
|
118
118
|
return None
|
|
119
119
|
|
|
120
|
+
# Directories that indicate a non-production entry point
|
|
121
|
+
_AUXILIARY_DIRS: frozenset[str] = frozenset({
|
|
122
|
+
"benchmark", "benchmarks", "bench",
|
|
123
|
+
"example", "examples",
|
|
124
|
+
"demo", "demos",
|
|
125
|
+
"playground", "playgrounds",
|
|
126
|
+
"fixture", "fixtures",
|
|
127
|
+
"sandbox", "e2e", "docs",
|
|
128
|
+
})
|
|
129
|
+
|
|
120
130
|
def _collect_entry_points(
|
|
121
131
|
self, context: DetectionContext, package_json: dict[str, Any]
|
|
122
132
|
) -> list[EntryPoint]:
|
|
123
133
|
entry_points: list[EntryPoint] = []
|
|
124
134
|
seen: set[str] = set()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
|
|
136
|
+
# Priority 1: package.json scripts — most reliable signal
|
|
137
|
+
scripts = package_json.get("scripts", {})
|
|
138
|
+
if isinstance(scripts, dict):
|
|
139
|
+
for script_name, script_cmd in scripts.items():
|
|
140
|
+
if not isinstance(script_cmd, str):
|
|
141
|
+
continue
|
|
142
|
+
ep_type, kind = self._classify_script(script_name)
|
|
143
|
+
if ep_type is None:
|
|
144
|
+
continue
|
|
145
|
+
# Extract file path from script command
|
|
146
|
+
path = self._extract_script_path(script_cmd, context)
|
|
147
|
+
if path and path not in seen and path_exists_in_tree(context.file_tree, path):
|
|
148
|
+
seen.add(path)
|
|
149
|
+
if not self._is_auxiliary_path(path):
|
|
150
|
+
entry_points.append(EntryPoint(
|
|
151
|
+
path=path,
|
|
152
|
+
stack="nodejs",
|
|
153
|
+
kind=kind,
|
|
154
|
+
source="package.json#scripts",
|
|
155
|
+
confidence="high",
|
|
156
|
+
reason=f"script:{script_name}",
|
|
157
|
+
evidence=f"scripts.{script_name} = {script_cmd!r:.80}",
|
|
158
|
+
entrypoint_type=ep_type,
|
|
159
|
+
))
|
|
160
|
+
|
|
161
|
+
# Priority 2: package.json bin — CLI production entry points
|
|
141
162
|
bin_field = package_json.get("bin")
|
|
163
|
+
bin_paths: list[str] = []
|
|
142
164
|
if isinstance(bin_field, str) and bin_field.strip():
|
|
143
|
-
|
|
165
|
+
bin_paths.append(bin_field.strip())
|
|
144
166
|
elif isinstance(bin_field, dict):
|
|
145
|
-
|
|
146
|
-
str(
|
|
167
|
+
bin_paths.extend(
|
|
168
|
+
str(v).strip() for v in bin_field.values()
|
|
169
|
+
if isinstance(v, str) and v.strip()
|
|
147
170
|
)
|
|
171
|
+
for path in bin_paths:
|
|
172
|
+
if path not in seen and path_exists_in_tree(context.file_tree, path):
|
|
173
|
+
seen.add(path)
|
|
174
|
+
entry_points.append(EntryPoint(
|
|
175
|
+
path=path,
|
|
176
|
+
stack="nodejs",
|
|
177
|
+
kind="cli",
|
|
178
|
+
source="package.json#bin",
|
|
179
|
+
confidence="high",
|
|
180
|
+
reason="bin",
|
|
181
|
+
evidence="declared in package.json bin field",
|
|
182
|
+
entrypoint_type="production",
|
|
183
|
+
))
|
|
148
184
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
185
|
+
# Priority 3: package.json main (library/module entry)
|
|
186
|
+
main = package_json.get("main")
|
|
187
|
+
if isinstance(main, str) and main.strip():
|
|
188
|
+
path = main.strip()
|
|
189
|
+
if path not in seen and path_exists_in_tree(context.file_tree, path):
|
|
190
|
+
seen.add(path)
|
|
191
|
+
entry_points.append(EntryPoint(
|
|
192
|
+
path=path,
|
|
193
|
+
stack="nodejs",
|
|
194
|
+
kind="server",
|
|
195
|
+
source="package.json",
|
|
196
|
+
confidence="high",
|
|
197
|
+
entrypoint_type="production",
|
|
198
|
+
))
|
|
161
199
|
|
|
162
|
-
|
|
200
|
+
# Priority 4: filename conventions (last resort — penalize auxiliary dirs)
|
|
201
|
+
for path in [
|
|
202
|
+
"server.js", "server.ts",
|
|
203
|
+
"src/index.js", "src/index.ts",
|
|
204
|
+
"src/main.js", "src/main.ts", "src/main.tsx",
|
|
205
|
+
"app/page.tsx", "pages/index.js",
|
|
206
|
+
]:
|
|
163
207
|
if path in seen or not path_exists_in_tree(context.file_tree, path):
|
|
164
208
|
continue
|
|
209
|
+
ep_type = self._path_entrypoint_type(path)
|
|
165
210
|
kind = "web" if path.startswith(("app/", "pages/")) else "server"
|
|
166
|
-
entry_points.append(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
211
|
+
entry_points.append(EntryPoint(
|
|
212
|
+
path=path,
|
|
213
|
+
stack="nodejs",
|
|
214
|
+
kind=kind,
|
|
215
|
+
source="convention",
|
|
216
|
+
confidence="medium",
|
|
217
|
+
reason="convention",
|
|
218
|
+
entrypoint_type=ep_type,
|
|
219
|
+
))
|
|
220
|
+
|
|
175
221
|
return entry_points
|
|
222
|
+
|
|
223
|
+
def _classify_script(self, script_name: str) -> tuple[str | None, str]:
|
|
224
|
+
"""Map script name → (entrypoint_type, kind). Returns (None, '') to skip."""
|
|
225
|
+
lower = script_name.lower()
|
|
226
|
+
if lower in ("start", "serve"):
|
|
227
|
+
return "production", "server"
|
|
228
|
+
if lower in ("dev", "develop", "watch"):
|
|
229
|
+
return "development", "server"
|
|
230
|
+
if lower in ("cli", "bin"):
|
|
231
|
+
return "production", "cli"
|
|
232
|
+
if "benchmark" in lower or lower == "bench":
|
|
233
|
+
return "benchmark", "script"
|
|
234
|
+
if lower.startswith("example") or lower.startswith("demo"):
|
|
235
|
+
return "example", "script"
|
|
236
|
+
return None, ""
|
|
237
|
+
|
|
238
|
+
def _extract_script_path(self, cmd: str, context: DetectionContext) -> str | None:
|
|
239
|
+
"""Extract a likely source file path from a script command string."""
|
|
240
|
+
import shlex
|
|
241
|
+
try:
|
|
242
|
+
parts = shlex.split(cmd)
|
|
243
|
+
except ValueError:
|
|
244
|
+
parts = cmd.split()
|
|
245
|
+
# Skip executor prefixes: node, ts-node, tsx, nodemon, npx, etc.
|
|
246
|
+
_SKIP = {"node", "ts-node", "tsx", "nodemon", "npx", "pnpm", "yarn", "npm", "bun",
|
|
247
|
+
"--inspect", "--inspect-brk", "--require", "-r", "run", "exec"}
|
|
248
|
+
for part in parts:
|
|
249
|
+
if part.startswith("-") or part in _SKIP or "=" in part:
|
|
250
|
+
continue
|
|
251
|
+
# It looks like a file path (has slash or known extension)
|
|
252
|
+
p = part.strip("'\"")
|
|
253
|
+
if ("/" in p or p.endswith((".js", ".ts", ".mjs", ".cjs"))) and not p.startswith("@"):
|
|
254
|
+
return p
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
def _is_auxiliary_path(self, path: str) -> bool:
|
|
258
|
+
norm = path.replace("\\", "/")
|
|
259
|
+
parts = norm.split("/")
|
|
260
|
+
return any(p.lower() in self._AUXILIARY_DIRS for p in parts)
|
|
261
|
+
|
|
262
|
+
def _path_entrypoint_type(self, path: str) -> str:
|
|
263
|
+
if self._is_auxiliary_path(path):
|
|
264
|
+
return "example"
|
|
265
|
+
return "production"
|
sourcecode/graph_analyzer.py
CHANGED
|
@@ -15,6 +15,44 @@ GraphDetail = Literal["high", "medium", "full"]
|
|
|
15
15
|
GraphImportance = Literal["high", "medium", "low"]
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
# Node.js stdlib modules — omit from graph unless --graph-detail full
|
|
19
|
+
_NODE_STDLIB: frozenset[str] = frozenset({
|
|
20
|
+
"fs", "fs/promises", "path", "os", "child_process", "process",
|
|
21
|
+
"net", "http", "https", "http2", "stream", "events", "util",
|
|
22
|
+
"crypto", "buffer", "url", "querystring", "string_decoder",
|
|
23
|
+
"readline", "repl", "assert", "dns", "dgram", "vm",
|
|
24
|
+
"worker_threads", "cluster", "module", "v8", "perf_hooks",
|
|
25
|
+
"async_hooks", "inspector", "tty", "zlib", "domain",
|
|
26
|
+
"timers", "timers/promises", "console", "constants",
|
|
27
|
+
"node:fs", "node:path", "node:os", "node:crypto", "node:buffer",
|
|
28
|
+
"node:stream", "node:events", "node:util", "node:http", "node:https",
|
|
29
|
+
"node:child_process", "node:process", "node:worker_threads",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
# Python stdlib modules — omit from graph unless --graph-detail full
|
|
33
|
+
_PYTHON_STDLIB: frozenset[str] = frozenset({
|
|
34
|
+
"os", "sys", "re", "io", "abc", "ast", "copy", "csv",
|
|
35
|
+
"enum", "functools", "itertools", "json", "logging", "math",
|
|
36
|
+
"pathlib", "random", "shutil", "string", "subprocess",
|
|
37
|
+
"threading", "time", "typing", "uuid", "warnings",
|
|
38
|
+
"collections", "contextlib", "dataclasses", "datetime",
|
|
39
|
+
"decimal", "difflib", "email", "glob", "hashlib", "hmac",
|
|
40
|
+
"http", "importlib", "inspect", "operator", "pickle",
|
|
41
|
+
"platform", "pprint", "queue", "signal", "socket",
|
|
42
|
+
"sqlite3", "stat", "struct", "tempfile", "textwrap",
|
|
43
|
+
"traceback", "unicodedata", "unittest", "urllib", "weakref",
|
|
44
|
+
"xml", "zipfile", "zlib",
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
# External packages that add noise without structural signal
|
|
48
|
+
_NODE_NOISE_PACKAGES: frozenset[str] = frozenset({
|
|
49
|
+
"lodash", "moment", "date-fns", "uuid", "chalk", "debug",
|
|
50
|
+
"yargs", "commander", "dotenv", "cross-env",
|
|
51
|
+
"eslint", "prettier", "typescript", "ts-node",
|
|
52
|
+
"@types/node", "@types/react",
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
|
|
18
56
|
class GraphAnalyzer:
|
|
19
57
|
"""Construye un grafo estructural parcial y seguro del proyecto."""
|
|
20
58
|
|
|
@@ -908,7 +946,14 @@ class GraphAnalyzer:
|
|
|
908
946
|
)
|
|
909
947
|
)
|
|
910
948
|
else:
|
|
911
|
-
|
|
949
|
+
# Silently skip stdlib and noise packages — they add no structural signal
|
|
950
|
+
bare = spec.removeprefix("node:")
|
|
951
|
+
root_pkg = bare.split("/")[0]
|
|
952
|
+
if (spec not in _NODE_STDLIB
|
|
953
|
+
and bare not in _NODE_STDLIB
|
|
954
|
+
and root_pkg not in _NODE_STDLIB
|
|
955
|
+
and root_pkg not in _NODE_NOISE_PACKAGES):
|
|
956
|
+
limitations.append(f"node_external:{relative_path}:{spec}")
|
|
912
957
|
|
|
913
958
|
return nodes, edges, limitations
|
|
914
959
|
|
|
@@ -1121,6 +1166,10 @@ class GraphAnalyzer:
|
|
|
1121
1166
|
def _resolve_python_import(
|
|
1122
1167
|
self, module_name: str, module_map: dict[str, str]
|
|
1123
1168
|
) -> Optional[str]:
|
|
1169
|
+
# Skip stdlib modules — they add no structural signal to the project graph
|
|
1170
|
+
root_module = module_name.split(".")[0]
|
|
1171
|
+
if root_module in _PYTHON_STDLIB:
|
|
1172
|
+
return None
|
|
1124
1173
|
for path, candidate in module_map.items():
|
|
1125
1174
|
if candidate == module_name:
|
|
1126
1175
|
return path
|
sourcecode/prepare_context.py
CHANGED
|
@@ -359,10 +359,15 @@ class TaskContextBuilder:
|
|
|
359
359
|
|
|
360
360
|
# ── 2. Detect stacks + entry points ───────────────────────────────
|
|
361
361
|
from sourcecode.detectors import ProjectDetector, build_default_detectors
|
|
362
|
+
from sourcecode.workspace import WorkspaceAnalyzer
|
|
362
363
|
|
|
363
364
|
detector = ProjectDetector(build_default_detectors())
|
|
365
|
+
workspace_analysis = WorkspaceAnalyzer().analyze(self.root, manifests)
|
|
364
366
|
stacks, entry_points, _ = detector.detect(self.root, file_tree, manifests)
|
|
365
|
-
stacks, project_type = detector.classify_results(
|
|
367
|
+
stacks, project_type = detector.classify_results(
|
|
368
|
+
file_tree, stacks, entry_points,
|
|
369
|
+
project_type_override="monorepo" if workspace_analysis.is_monorepo else None,
|
|
370
|
+
)
|
|
366
371
|
|
|
367
372
|
# ── 3. Summarize ───────────────────────────────────────────────────
|
|
368
373
|
from sourcecode.schema import AnalysisMetadata, SourceMap
|
|
@@ -378,6 +383,14 @@ class TaskContextBuilder:
|
|
|
378
383
|
)
|
|
379
384
|
sm.file_paths = all_paths
|
|
380
385
|
|
|
386
|
+
# Classify workspace packages for structural context
|
|
387
|
+
if workspace_analysis.workspaces:
|
|
388
|
+
from sourcecode.runtime_classifier import RuntimeClassifier
|
|
389
|
+
sm.monorepo_packages = RuntimeClassifier().classify(
|
|
390
|
+
self.root,
|
|
391
|
+
[ws.path for ws in workspace_analysis.workspaces],
|
|
392
|
+
)
|
|
393
|
+
|
|
381
394
|
project_summary = ProjectSummarizer(self.root).generate(sm)
|
|
382
395
|
architecture_summary = ArchitectureSummarizer(self.root).generate(sm)
|
|
383
396
|
|
|
@@ -436,13 +449,31 @@ class TaskContextBuilder:
|
|
|
436
449
|
for p, n in sorted(counts2.items(), key=lambda x: -x[1])[:8]
|
|
437
450
|
]
|
|
438
451
|
|
|
452
|
+
# ── 5b. Git signals for ranking ────────────────────────────────────
|
|
453
|
+
git_hotspots: dict[str, int] = {}
|
|
454
|
+
uncommitted_files: set[str] = set()
|
|
455
|
+
try:
|
|
456
|
+
from sourcecode.git_analyzer import GitAnalyzer
|
|
457
|
+
_gc = GitAnalyzer().analyze(self.root, depth=30, days=90)
|
|
458
|
+
_bad = {"no_git_repo", "git_not_found", "git_timeout"}
|
|
459
|
+
if _gc and not (_bad & set(_gc.limitations)):
|
|
460
|
+
git_hotspots = {h.file: h.commit_count for h in _gc.change_hotspots}
|
|
461
|
+
if _gc.uncommitted_changes:
|
|
462
|
+
_uc = _gc.uncommitted_changes
|
|
463
|
+
uncommitted_files = set(_uc.staged) | set(_uc.unstaged)
|
|
464
|
+
except Exception:
|
|
465
|
+
pass
|
|
466
|
+
|
|
439
467
|
# ── 6. Rank files ──────────────────────────────────────────────────
|
|
440
468
|
entry_set = {ep.path for ep in entry_points}
|
|
441
469
|
test_set = {p for p in all_paths if self._is_test(p)}
|
|
442
470
|
source_set = {p for p in all_paths if not self._is_test(p) and self._is_source(p)}
|
|
443
471
|
|
|
444
472
|
relevant_files = self._rank_files(
|
|
445
|
-
task_name, spec, all_paths, entry_set, test_set
|
|
473
|
+
task_name, spec, all_paths, entry_set, test_set,
|
|
474
|
+
monorepo_packages=sm.monorepo_packages if sm.monorepo_packages else None,
|
|
475
|
+
git_hotspots=git_hotspots,
|
|
476
|
+
uncommitted_files=uncommitted_files,
|
|
446
477
|
)
|
|
447
478
|
|
|
448
479
|
# ── 7. Test gaps (generate-tests only) ────────────────────────────
|
|
@@ -590,7 +621,21 @@ class TaskContextBuilder:
|
|
|
590
621
|
all_paths: list[str],
|
|
591
622
|
entry_set: set[str],
|
|
592
623
|
test_set: set[str],
|
|
624
|
+
monorepo_packages: Optional[list] = None,
|
|
625
|
+
git_hotspots: Optional[dict[str, int]] = None,
|
|
626
|
+
uncommitted_files: Optional[set[str]] = None,
|
|
593
627
|
) -> list[RelevantFile]:
|
|
628
|
+
from sourcecode.relevance_scorer import RelevanceScorer
|
|
629
|
+
scorer = RelevanceScorer(monorepo_packages or [])
|
|
630
|
+
|
|
631
|
+
# Auxiliary entry points (benchmark, docs, examples) must not get
|
|
632
|
+
# the production entry boost — they are not runtime signals.
|
|
633
|
+
runtime_entry_set = {ep for ep in entry_set if not scorer.is_auxiliary(ep)}
|
|
634
|
+
|
|
635
|
+
_hotspots = git_hotspots or {}
|
|
636
|
+
_uncommitted = uncommitted_files or set()
|
|
637
|
+
_max_churn = max(_hotspots.values(), default=1)
|
|
638
|
+
|
|
594
639
|
scored: list[tuple[float, RelevantFile]] = []
|
|
595
640
|
|
|
596
641
|
for path in all_paths:
|
|
@@ -599,6 +644,10 @@ class TaskContextBuilder:
|
|
|
599
644
|
if any(pen in path for pen in spec.ranking_penalties):
|
|
600
645
|
continue
|
|
601
646
|
|
|
647
|
+
# Hard filter: tooling/config noise
|
|
648
|
+
if scorer.is_noise(path):
|
|
649
|
+
continue
|
|
650
|
+
|
|
602
651
|
is_test = path in test_set
|
|
603
652
|
if is_test and task_name != "generate-tests":
|
|
604
653
|
continue
|
|
@@ -606,7 +655,8 @@ class TaskContextBuilder:
|
|
|
606
655
|
score = 0.0
|
|
607
656
|
reasons: list[str] = []
|
|
608
657
|
|
|
609
|
-
|
|
658
|
+
# Only runtime entry points get the production boost
|
|
659
|
+
if path in runtime_entry_set:
|
|
610
660
|
score += 3.0
|
|
611
661
|
reasons.append("entry point")
|
|
612
662
|
|
|
@@ -625,11 +675,30 @@ class TaskContextBuilder:
|
|
|
625
675
|
if not reasons:
|
|
626
676
|
reasons.append("source file")
|
|
627
677
|
|
|
678
|
+
# Operational relevance boost/penalty from package role
|
|
679
|
+
rel = scorer.score(path)
|
|
680
|
+
score += (rel - 0.3) * 2.0 # center around 0.3 baseline
|
|
681
|
+
|
|
682
|
+
# Suppress auxiliary dirs (benchmarks, docs, examples, demos)
|
|
683
|
+
if scorer.is_auxiliary(path):
|
|
684
|
+
score -= 2.0
|
|
685
|
+
|
|
686
|
+
# Git churn: frequently changed files are high-signal for active work
|
|
687
|
+
churn = _hotspots.get(path, 0)
|
|
688
|
+
if churn > 0:
|
|
689
|
+
score += (churn / _max_churn) * 1.5
|
|
690
|
+
reasons.append(f"git churn ({churn})")
|
|
691
|
+
|
|
692
|
+
# Uncommitted changes: files actively being edited rank highest
|
|
693
|
+
if path in _uncommitted:
|
|
694
|
+
score += 1.0
|
|
695
|
+
reasons.append("uncommitted changes")
|
|
696
|
+
|
|
628
697
|
if score <= 0:
|
|
629
698
|
continue
|
|
630
699
|
|
|
631
700
|
role = (
|
|
632
|
-
"entrypoint" if path in
|
|
701
|
+
"entrypoint" if path in runtime_entry_set
|
|
633
702
|
else ("test" if is_test else "source")
|
|
634
703
|
)
|
|
635
704
|
scored.append((score, RelevantFile(
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Operational relevance scoring for files and directories.
|
|
4
|
+
|
|
5
|
+
Returns a (relevance, noise) pair per path:
|
|
6
|
+
relevance: 0.0–1.0 — how useful is this for an agent modifying code
|
|
7
|
+
noise: bool — should this be suppressed unless explicitly requested
|
|
8
|
+
|
|
9
|
+
Key principle: signal over inventory. Tooling, config, and auxiliary content
|
|
10
|
+
score low. Runtime core, entrypoints, and central modules score high.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from sourcecode.schema import MonorepoPackageInfo
|
|
18
|
+
|
|
19
|
+
# --------------------------------------------------------------------------
|
|
20
|
+
# Noise path patterns — suppress unless explicitly needed
|
|
21
|
+
# --------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
_NOISE_PREFIXES: frozenset[str] = frozenset({
|
|
24
|
+
"node_modules/",
|
|
25
|
+
".git/",
|
|
26
|
+
"__pycache__/",
|
|
27
|
+
".venv/",
|
|
28
|
+
"venv/",
|
|
29
|
+
".mypy_cache/",
|
|
30
|
+
".pytest_cache/",
|
|
31
|
+
"dist/",
|
|
32
|
+
"build/",
|
|
33
|
+
".turbo/",
|
|
34
|
+
".next/",
|
|
35
|
+
".nuxt/",
|
|
36
|
+
"coverage/",
|
|
37
|
+
".nyc_output/",
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
_NOISE_DIRS: frozenset[str] = frozenset({
|
|
41
|
+
"node_modules", "__pycache__", ".git", "dist", "build",
|
|
42
|
+
".turbo", "coverage", ".nyc_output", ".next", ".nuxt",
|
|
43
|
+
".venv", "venv", ".mypy_cache", ".pytest_cache",
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
_NOISE_SUFFIXES: frozenset[str] = frozenset({
|
|
47
|
+
".lock", ".log", ".map", ".min.js", ".min.css",
|
|
48
|
+
".snap", ".d.ts.map",
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
_TOOLING_FILENAMES: frozenset[str] = frozenset({
|
|
52
|
+
".eslintrc", ".eslintrc.js", ".eslintrc.json", ".eslintrc.yaml", ".eslintrc.yml",
|
|
53
|
+
".prettierrc", ".prettierrc.js", ".prettierrc.json",
|
|
54
|
+
"prettier.config.js", "prettier.config.ts",
|
|
55
|
+
"eslint.config.js", "eslint.config.ts",
|
|
56
|
+
".editorconfig", ".gitignore", ".gitattributes",
|
|
57
|
+
"commitlint.config.js", "stylelint.config.js",
|
|
58
|
+
"jest.config.js", "jest.config.ts", "jest.config.json",
|
|
59
|
+
"vitest.config.ts", "vitest.config.js",
|
|
60
|
+
"webpack.config.js", "webpack.config.ts",
|
|
61
|
+
"vite.config.ts", "vite.config.js",
|
|
62
|
+
"rollup.config.js", "rollup.config.ts",
|
|
63
|
+
"babel.config.js", "babel.config.json",
|
|
64
|
+
".babelrc", ".babelrc.js",
|
|
65
|
+
"tsconfig.json", "tsconfig.base.json",
|
|
66
|
+
".dockerignore", "Makefile",
|
|
67
|
+
"lerna.json", "nx.json", "turbo.json",
|
|
68
|
+
"CHANGELOG.md", "LICENSE", "LICENSE.md",
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
_AUXILIARY_DIR_PATTERNS: list[re.Pattern[str]] = [
|
|
72
|
+
re.compile(r"(?:^|/)benchmark[s]?(?:/|$)"),
|
|
73
|
+
re.compile(r"(?:^|/)example[s]?(?:/|$)"),
|
|
74
|
+
re.compile(r"(?:^|/)demo[s]?(?:/|$)"),
|
|
75
|
+
re.compile(r"(?:^|/)playground[s]?(?:/|$)"),
|
|
76
|
+
re.compile(r"(?:^|/)fixture[s]?(?:/|$)"),
|
|
77
|
+
re.compile(r"(?:^|/)sandbox(?:/|$)"),
|
|
78
|
+
re.compile(r"(?:^|/)docs?(?:/|$)"),
|
|
79
|
+
re.compile(r"(?:^|/)\.github(?:/|$)"),
|
|
80
|
+
re.compile(r"(?:^|/)\.claude(?:/|$)"),
|
|
81
|
+
re.compile(r"(?:^|/)\.vscode(?:/|$)"),
|
|
82
|
+
re.compile(r"(?:^|/)scripts?(?:/|$)"),
|
|
83
|
+
re.compile(r"(?:^|/)tools?(?:/|$)"),
|
|
84
|
+
re.compile(r"(?:^|/)ci(?:/|$)"),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
_HIGH_VALUE_SUFFIXES: frozenset[str] = frozenset({
|
|
88
|
+
".py", ".ts", ".tsx", ".js", ".jsx", ".mjs",
|
|
89
|
+
".go", ".java", ".kt", ".rs", ".rb", ".cs",
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
_ENTRYPOINT_STEMS: frozenset[str] = frozenset({
|
|
93
|
+
"main", "cli", "app", "server", "index", "__main__",
|
|
94
|
+
"application", "bootstrap", "entry",
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class RelevanceScorer:
|
|
99
|
+
"""Scores file paths by operational relevance for AI agents."""
|
|
100
|
+
|
|
101
|
+
def __init__(self, monorepo_packages: Optional[list[MonorepoPackageInfo]] = None) -> None:
|
|
102
|
+
self._pkg_roles: dict[str, str] = {} # path_prefix → role
|
|
103
|
+
if monorepo_packages:
|
|
104
|
+
for pkg in monorepo_packages:
|
|
105
|
+
prefix = pkg.path.rstrip("/") + "/"
|
|
106
|
+
self._pkg_roles[prefix] = pkg.architectural_role
|
|
107
|
+
|
|
108
|
+
def score(self, path: str) -> float:
|
|
109
|
+
"""Return operational relevance 0.0–1.0. Higher = more useful for agents."""
|
|
110
|
+
norm = path.replace("\\", "/").lstrip("/")
|
|
111
|
+
|
|
112
|
+
if self.is_noise(norm):
|
|
113
|
+
return 0.0
|
|
114
|
+
|
|
115
|
+
base = 0.3
|
|
116
|
+
|
|
117
|
+
# Package role boost
|
|
118
|
+
role = self._package_role(norm)
|
|
119
|
+
role_boost = {
|
|
120
|
+
"runtime_core": 0.4,
|
|
121
|
+
"plugin_host": 0.35,
|
|
122
|
+
"backend_runtime": 0.3,
|
|
123
|
+
"frontend_runtime": 0.25,
|
|
124
|
+
"composition_layer": 0.2,
|
|
125
|
+
"plugin_package": 0.15,
|
|
126
|
+
"infrastructure_layer": 0.15,
|
|
127
|
+
"tooling_layer": -0.1,
|
|
128
|
+
"docs_layer": -0.15,
|
|
129
|
+
"test_layer": 0.05,
|
|
130
|
+
"benchmark_layer": -0.2,
|
|
131
|
+
}.get(role, 0.0)
|
|
132
|
+
base += role_boost
|
|
133
|
+
|
|
134
|
+
# Source file boost
|
|
135
|
+
suffix = Path(norm).suffix.lower()
|
|
136
|
+
if suffix in _HIGH_VALUE_SUFFIXES:
|
|
137
|
+
base += 0.1
|
|
138
|
+
|
|
139
|
+
# Entrypoint stem boost
|
|
140
|
+
stem = Path(norm).stem.lower()
|
|
141
|
+
if stem in _ENTRYPOINT_STEMS:
|
|
142
|
+
base += 0.15
|
|
143
|
+
|
|
144
|
+
# Penalize auxiliary dirs
|
|
145
|
+
if self._is_auxiliary(norm):
|
|
146
|
+
base -= 0.2
|
|
147
|
+
|
|
148
|
+
return max(0.0, min(1.0, base))
|
|
149
|
+
|
|
150
|
+
def is_noise(self, path: str) -> bool:
|
|
151
|
+
"""True if this file should be suppressed from default agent output."""
|
|
152
|
+
norm = path.replace("\\", "/").lstrip("/")
|
|
153
|
+
|
|
154
|
+
if any(norm.startswith(p) for p in _NOISE_PREFIXES):
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
parts = norm.split("/")
|
|
158
|
+
if any(p in _NOISE_DIRS for p in parts):
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
filename = Path(norm).name
|
|
162
|
+
if filename in _TOOLING_FILENAMES:
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
for suffix in _NOISE_SUFFIXES:
|
|
166
|
+
if norm.endswith(suffix):
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
def is_auxiliary(self, path: str) -> bool:
|
|
172
|
+
"""True if file is in a benchmark/example/demo/docs directory."""
|
|
173
|
+
return self._is_auxiliary(path.replace("\\", "/"))
|
|
174
|
+
|
|
175
|
+
def _is_auxiliary(self, norm: str) -> bool:
|
|
176
|
+
return any(p.search(norm) for p in _AUXILIARY_DIR_PATTERNS)
|
|
177
|
+
|
|
178
|
+
def _package_role(self, norm: str) -> str:
|
|
179
|
+
for prefix, role in self._pkg_roles.items():
|
|
180
|
+
if norm.startswith(prefix):
|
|
181
|
+
return role
|
|
182
|
+
return "unknown"
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Classifies workspace packages into architectural roles using weighted inference.
|
|
4
|
+
|
|
5
|
+
Signal priority (descending):
|
|
6
|
+
scripts.start/serve → runtime_core/backend_runtime
|
|
7
|
+
scripts.cli/bin → runtime_core (CLI tool)
|
|
8
|
+
runtime dep imports → backend_runtime / frontend_runtime
|
|
9
|
+
name/path patterns → specialized roles
|
|
10
|
+
fan-in → weak corroboration only, NEVER sole driver
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from sourcecode.schema import MonorepoPackageInfo
|
|
19
|
+
|
|
20
|
+
# --------------------------------------------------------------------------
|
|
21
|
+
# Noise / negative patterns — packages matching these are NOT runtime_core
|
|
22
|
+
# regardless of fan-in.
|
|
23
|
+
# --------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
_NEGATIVE_RUNTIME_CORE: list[re.Pattern[str]] = [
|
|
26
|
+
re.compile(r"(?:^|/)utils?(?:/|$)"),
|
|
27
|
+
re.compile(r"(?:^|/)shared(?:/|$)"),
|
|
28
|
+
re.compile(r"(?:^|/)types?(?:/|$)"),
|
|
29
|
+
re.compile(r"(?:^|/)config(?:s)?(?:/|$)"),
|
|
30
|
+
re.compile(r"(?:^|/)common(?:/|$)"),
|
|
31
|
+
re.compile(r"(?:^|/)helpers?(?:/|$)"),
|
|
32
|
+
re.compile(r"(?:^|/)constants?(?:/|$)"),
|
|
33
|
+
re.compile(r"(?:^|/)fixture[s]?(?:/|$)"),
|
|
34
|
+
re.compile(r"(?:^|/)tooling(?:/|$)"),
|
|
35
|
+
re.compile(r"(?:^|/)preset[s]?(?:/|$)"),
|
|
36
|
+
re.compile(r"(?:^|/)theme[s]?(?:/|$)"),
|
|
37
|
+
re.compile(r"(?:^|/)i18n(?:/|$)"),
|
|
38
|
+
re.compile(r"(?:^|/)locale[s]?(?:/|$)"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
_NEGATIVE_NAME_RUNTIME_CORE: list[re.Pattern[str]] = [
|
|
42
|
+
re.compile(r"\butils?\b"),
|
|
43
|
+
re.compile(r"\bshared\b"),
|
|
44
|
+
re.compile(r"\btypes?\b"),
|
|
45
|
+
re.compile(r"\bconfig\b"),
|
|
46
|
+
re.compile(r"\bcommon\b"),
|
|
47
|
+
re.compile(r"\bhelpers?\b"),
|
|
48
|
+
re.compile(r"\bconstants?\b"),
|
|
49
|
+
re.compile(r"\bfixtures?\b"),
|
|
50
|
+
re.compile(r"\btooling\b"),
|
|
51
|
+
re.compile(r"\btheme[s]?\b"),
|
|
52
|
+
re.compile(r"\bi18n\b"),
|
|
53
|
+
re.compile(r"\blocale[s]?\b"),
|
|
54
|
+
re.compile(r"\bpreset[s]?\b"),
|
|
55
|
+
re.compile(r"eslint|prettier|lint|format"),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# --------------------------------------------------------------------------
|
|
59
|
+
# Role-determining pattern tables
|
|
60
|
+
# --------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
_PATH_SIGNALS: list[tuple[re.Pattern[str], str, int]] = [
|
|
63
|
+
(re.compile(r"benchmark|bench"), "benchmark_layer", 4),
|
|
64
|
+
(re.compile(r"(?:^|/)docs?(?:/|$)"), "docs_layer", 4),
|
|
65
|
+
(re.compile(r"(?:^|/)tests?(?:/|$)|(?:^|/)specs?(?:/|$)|(?:^|/)e2e(?:/|$)"), "test_layer", 4),
|
|
66
|
+
(re.compile(r"example|demo|playground|fixture|sandbox"), "benchmark_layer", 3),
|
|
67
|
+
(re.compile(r"(?:^|/)plugins?(?:/|$)"), "plugin_package", 3),
|
|
68
|
+
(re.compile(r"(?:^|/)presets?(?:/|$)"), "composition_layer", 3),
|
|
69
|
+
(re.compile(r"infra(?:structure)?"), "infrastructure_layer", 2),
|
|
70
|
+
(re.compile(r"(?:^|/)client(?:/|$)|(?:^|/)web(?:/|$)|(?:^|/)ui(?:/|$)"), "frontend_runtime", 2),
|
|
71
|
+
(re.compile(r"(?:^|/)server(?:/|$)"), "backend_runtime", 3),
|
|
72
|
+
(re.compile(r"(?:^|/)app(?:/|$)"), "backend_runtime", 2),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
_NAME_SIGNALS: list[tuple[re.Pattern[str], str, int]] = [
|
|
76
|
+
(re.compile(r"benchmark|bench"), "benchmark_layer", 4),
|
|
77
|
+
(re.compile(r"\bplugin"), "plugin_package", 3),
|
|
78
|
+
(re.compile(r"\bpreset"), "composition_layer", 3),
|
|
79
|
+
(re.compile(r"client|web|react|frontend|\bui\b"), "frontend_runtime", 2),
|
|
80
|
+
(re.compile(r"\bserver\b|\bapi\b|\bbackend\b"), "backend_runtime", 3),
|
|
81
|
+
(re.compile(r"database|db|storage|cache|queue|redis|pg|sql"), "infrastructure_layer", 2),
|
|
82
|
+
(re.compile(r"\bcore\b|\bkernel\b|\bruntime\b|\bengine\b|\bapp\b"), "runtime_core", 2),
|
|
83
|
+
(re.compile(r"test|spec|e2e"), "test_layer", 3),
|
|
84
|
+
(re.compile(r"lint|format|build|webpack|vite|eslint|prettier|tsconfig"), "tooling_layer", 3),
|
|
85
|
+
(re.compile(r"docs?$"), "docs_layer", 3),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# Scripts that provide hard runtime evidence
|
|
89
|
+
_RUNTIME_SCRIPTS: frozenset[str] = frozenset({"start", "serve", "server"})
|
|
90
|
+
_DEV_SCRIPTS: frozenset[str] = frozenset({"dev", "develop", "watch"})
|
|
91
|
+
_CLI_SCRIPTS: frozenset[str] = frozenset({"cli", "bin", "run"})
|
|
92
|
+
|
|
93
|
+
_SCRIPT_SIGNALS: dict[str, tuple[str, int]] = {
|
|
94
|
+
"start": ("backend_runtime", 4),
|
|
95
|
+
"serve": ("backend_runtime", 4),
|
|
96
|
+
"server": ("backend_runtime", 4),
|
|
97
|
+
"cli": ("backend_runtime", 3),
|
|
98
|
+
"dev": ("backend_runtime", 2),
|
|
99
|
+
"develop": ("backend_runtime", 2),
|
|
100
|
+
"benchmark": ("benchmark_layer", 4),
|
|
101
|
+
"bench": ("benchmark_layer", 4),
|
|
102
|
+
"test": ("test_layer", 3),
|
|
103
|
+
"lint": ("tooling_layer", 2),
|
|
104
|
+
"format": ("tooling_layer", 2),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_DEP_SIGNALS: list[tuple[re.Pattern[str], str, int]] = [
|
|
108
|
+
(re.compile(r"^react$|^vue$|^@angular/core$|^svelte$"), "frontend_runtime", 3),
|
|
109
|
+
(re.compile(r"^express$|^fastify$|^koa$|^@nestjs/core$|^hono$|^elysia$"), "backend_runtime", 3),
|
|
110
|
+
(re.compile(r"^sequelize$|^prisma$|^typeorm$|^mongoose$|^knex$|^pg$"),"infrastructure_layer",2),
|
|
111
|
+
(re.compile(r"^jest$|^mocha$|^vitest$|^cypress$|^playwright$"), "test_layer", 3),
|
|
112
|
+
(re.compile(r"^eslint$|^prettier$"), "tooling_layer", 2),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
_ROLE_PRIORITY: dict[str, int] = {
|
|
116
|
+
"runtime_core": 10,
|
|
117
|
+
"plugin_host": 9,
|
|
118
|
+
"backend_runtime": 8,
|
|
119
|
+
"composition_layer": 7,
|
|
120
|
+
"frontend_runtime": 6,
|
|
121
|
+
"plugin_package": 5,
|
|
122
|
+
"infrastructure_layer": 4,
|
|
123
|
+
"test_layer": 3,
|
|
124
|
+
"docs_layer": 3,
|
|
125
|
+
"tooling_layer": 3,
|
|
126
|
+
"benchmark_layer": 3,
|
|
127
|
+
"unknown": 0,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_NOISE_ROLES = frozenset({"benchmark_layer", "test_layer", "docs_layer", "tooling_layer"})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class RuntimeClassifier:
|
|
134
|
+
"""Classifies workspace packages by architectural role."""
|
|
135
|
+
|
|
136
|
+
def classify(self, root: Path, workspace_paths: list[str]) -> list[MonorepoPackageInfo]:
|
|
137
|
+
raw: list[tuple[str, dict[str, Any]]] = []
|
|
138
|
+
for ws_path in workspace_paths:
|
|
139
|
+
pkg_root = root / ws_path
|
|
140
|
+
if pkg_root.is_dir():
|
|
141
|
+
raw.append((ws_path, self._load_pkg_json(pkg_root)))
|
|
142
|
+
|
|
143
|
+
fan_in = self._compute_fan_in(raw)
|
|
144
|
+
results: list[MonorepoPackageInfo] = []
|
|
145
|
+
|
|
146
|
+
for ws_path, pkg_json in raw:
|
|
147
|
+
name = str(pkg_json.get("name", ws_path)) if pkg_json else ws_path
|
|
148
|
+
role, signals, conf = self._score_package(ws_path, name, pkg_json, fan_in.get(ws_path, 0))
|
|
149
|
+
|
|
150
|
+
# plugin_host detection (independent of runtime_core path)
|
|
151
|
+
if role not in _NOISE_ROLES and self._detect_plugin_host(pkg_json, ws_path, fan_in.get(ws_path, 0)):
|
|
152
|
+
role = "plugin_host"
|
|
153
|
+
signals.append("plugin_host:inferred")
|
|
154
|
+
|
|
155
|
+
criticality = self._criticality(role, fan_in.get(ws_path, 0))
|
|
156
|
+
results.append(MonorepoPackageInfo(
|
|
157
|
+
path=ws_path,
|
|
158
|
+
name=name,
|
|
159
|
+
architectural_role=role,
|
|
160
|
+
criticality=criticality,
|
|
161
|
+
confidence=conf,
|
|
162
|
+
role_signals=signals,
|
|
163
|
+
))
|
|
164
|
+
|
|
165
|
+
return results
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
# Internal helpers
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def _load_pkg_json(self, pkg_root: Path) -> dict[str, Any]:
|
|
172
|
+
try:
|
|
173
|
+
content = (pkg_root / "package.json").read_text(encoding="utf-8", errors="replace")
|
|
174
|
+
return json.loads(content) # type: ignore[no-any-return]
|
|
175
|
+
except Exception:
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
def _compute_fan_in(self, raw: list[tuple[str, dict[str, Any]]]) -> dict[str, int]:
|
|
179
|
+
name_to_path: dict[str, str] = {}
|
|
180
|
+
for ws_path, pkg_json in raw:
|
|
181
|
+
name = str(pkg_json.get("name", "")) if pkg_json else ""
|
|
182
|
+
if name:
|
|
183
|
+
name_to_path[name] = ws_path
|
|
184
|
+
|
|
185
|
+
fan_in: dict[str, int] = {}
|
|
186
|
+
for _, pkg_json in raw:
|
|
187
|
+
if not pkg_json:
|
|
188
|
+
continue
|
|
189
|
+
for field in ("dependencies", "devDependencies", "peerDependencies"):
|
|
190
|
+
for dep_name in pkg_json.get(field, {}):
|
|
191
|
+
target_path = name_to_path.get(str(dep_name))
|
|
192
|
+
if target_path:
|
|
193
|
+
fan_in[target_path] = fan_in.get(target_path, 0) + 1
|
|
194
|
+
return fan_in
|
|
195
|
+
|
|
196
|
+
def _has_runtime_evidence(self, pkg_json: dict[str, Any], ws_path: str) -> bool:
|
|
197
|
+
"""True only when package has explicit evidence of being a running process."""
|
|
198
|
+
if not pkg_json:
|
|
199
|
+
return False
|
|
200
|
+
# bin field = CLI/server executable
|
|
201
|
+
if pkg_json.get("bin"):
|
|
202
|
+
return True
|
|
203
|
+
# start/serve/server script = runtime entry point
|
|
204
|
+
scripts = pkg_json.get("scripts") or {}
|
|
205
|
+
if isinstance(scripts, dict):
|
|
206
|
+
if _RUNTIME_SCRIPTS & set(scripts.keys()):
|
|
207
|
+
return True
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
def _is_utility_package(self, ws_path: str, name: str) -> bool:
|
|
211
|
+
"""True for packages that are libraries/utilities, NOT runtime processes."""
|
|
212
|
+
path_lower = ws_path.lower().replace("\\", "/")
|
|
213
|
+
name_lower = (name or "").lower()
|
|
214
|
+
return (
|
|
215
|
+
any(p.search(path_lower) for p in _NEGATIVE_RUNTIME_CORE)
|
|
216
|
+
or any(p.search(name_lower) for p in _NEGATIVE_NAME_RUNTIME_CORE)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def _score_package(
|
|
220
|
+
self, ws_path: str, name: str, pkg_json: dict[str, Any], fan_in: int
|
|
221
|
+
) -> tuple[str, list[str], str]:
|
|
222
|
+
scores: dict[str, float] = {}
|
|
223
|
+
signals: list[str] = []
|
|
224
|
+
|
|
225
|
+
path_lower = ws_path.lower().replace("\\", "/")
|
|
226
|
+
name_lower = name.lower() if name else ""
|
|
227
|
+
|
|
228
|
+
for pattern, role, weight in _PATH_SIGNALS:
|
|
229
|
+
if pattern.search(path_lower):
|
|
230
|
+
scores[role] = scores.get(role, 0) + weight
|
|
231
|
+
signals.append(f"path:{role}")
|
|
232
|
+
break # one path signal per package
|
|
233
|
+
|
|
234
|
+
for pattern, role, weight in _NAME_SIGNALS:
|
|
235
|
+
if pattern.search(name_lower):
|
|
236
|
+
scores[role] = scores.get(role, 0) + weight
|
|
237
|
+
signals.append(f"name:{role}")
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
if pkg_json:
|
|
241
|
+
scripts = pkg_json.get("scripts", {}) or {}
|
|
242
|
+
if isinstance(scripts, dict):
|
|
243
|
+
for script_name, (role, weight) in _SCRIPT_SIGNALS.items():
|
|
244
|
+
if script_name in scripts:
|
|
245
|
+
scores[role] = scores.get(role, 0) + weight
|
|
246
|
+
signals.append(f"script:{script_name}")
|
|
247
|
+
|
|
248
|
+
# bin = CLI tool or server executable; strong runtime signal
|
|
249
|
+
if pkg_json.get("bin"):
|
|
250
|
+
scores["backend_runtime"] = scores.get("backend_runtime", 0) + 3
|
|
251
|
+
signals.append("bin:declared")
|
|
252
|
+
|
|
253
|
+
# exports/main: weak lib signal only — do NOT boost runtime_core
|
|
254
|
+
if pkg_json.get("exports") or pkg_json.get("main"):
|
|
255
|
+
signals.append("exports:declared")
|
|
256
|
+
|
|
257
|
+
prod_deps: set[str] = set()
|
|
258
|
+
for field in ("dependencies", "peerDependencies"):
|
|
259
|
+
dep_map = pkg_json.get(field) or {}
|
|
260
|
+
if isinstance(dep_map, dict):
|
|
261
|
+
prod_deps.update(str(k) for k in dep_map)
|
|
262
|
+
|
|
263
|
+
for dep_name in prod_deps:
|
|
264
|
+
for pattern, role, weight in _DEP_SIGNALS:
|
|
265
|
+
if pattern.search(dep_name.lower()):
|
|
266
|
+
scores[role] = scores.get(role, 0) + weight
|
|
267
|
+
break
|
|
268
|
+
|
|
269
|
+
# Fan-in: weak corroboration only (+1 per point, capped at 2)
|
|
270
|
+
# Never sole driver — it boosts the CURRENT leading role, not runtime_core
|
|
271
|
+
if fan_in >= 2:
|
|
272
|
+
signals.append(f"fan_in:{fan_in}")
|
|
273
|
+
fan_boost = min(fan_in, 2)
|
|
274
|
+
if scores:
|
|
275
|
+
# Boost whatever role is currently winning
|
|
276
|
+
current_best = max(scores, key=lambda r: (scores[r], _ROLE_PRIORITY.get(r, 0)))
|
|
277
|
+
scores[current_best] = scores.get(current_best, 0) + fan_boost
|
|
278
|
+
# No scores yet (purely utility pkg): high fan-in → infrastructure_layer
|
|
279
|
+
else:
|
|
280
|
+
scores["infrastructure_layer"] = scores.get("infrastructure_layer", 0) + fan_boost
|
|
281
|
+
|
|
282
|
+
if not scores:
|
|
283
|
+
return "unknown", signals, "low"
|
|
284
|
+
|
|
285
|
+
best_role = max(scores, key=lambda r: (scores[r], _ROLE_PRIORITY.get(r, 0)))
|
|
286
|
+
best_score = scores[best_role]
|
|
287
|
+
|
|
288
|
+
# Promote backend_runtime → runtime_core only with explicit runtime evidence
|
|
289
|
+
# AND not a utility/shared package
|
|
290
|
+
if best_role == "backend_runtime" and self._has_runtime_evidence(pkg_json, ws_path):
|
|
291
|
+
if not self._is_utility_package(ws_path, name):
|
|
292
|
+
best_role = "runtime_core"
|
|
293
|
+
signals.append("promoted:has_runtime_evidence")
|
|
294
|
+
|
|
295
|
+
# Hard guard: runtime_core NEVER assigned to utility packages
|
|
296
|
+
if best_role == "runtime_core" and self._is_utility_package(ws_path, name):
|
|
297
|
+
# Downgrade: utility with high fan-in → infrastructure_layer
|
|
298
|
+
best_role = "infrastructure_layer"
|
|
299
|
+
signals.append("demoted:utility_package")
|
|
300
|
+
|
|
301
|
+
conf = "high" if best_score >= 6 else "medium" if best_score >= 3 else "low"
|
|
302
|
+
return best_role, signals, conf
|
|
303
|
+
|
|
304
|
+
def _detect_plugin_host(self, pkg_json: dict[str, Any], ws_path: str, fan_in: int) -> bool:
|
|
305
|
+
if fan_in < 2:
|
|
306
|
+
return False
|
|
307
|
+
path_lower = ws_path.lower()
|
|
308
|
+
if "plugin" in path_lower or "benchmark" in path_lower:
|
|
309
|
+
return False
|
|
310
|
+
if not pkg_json:
|
|
311
|
+
return False
|
|
312
|
+
name = str(pkg_json.get("name", "")).lower()
|
|
313
|
+
if "plugin" in name:
|
|
314
|
+
return False
|
|
315
|
+
scripts = pkg_json.get("scripts", {}) or {}
|
|
316
|
+
if isinstance(scripts, dict):
|
|
317
|
+
if any("plugin" in str(k).lower() or "plugin" in str(v).lower()
|
|
318
|
+
for k, v in scripts.items()):
|
|
319
|
+
return True
|
|
320
|
+
exports = pkg_json.get("exports") or {}
|
|
321
|
+
if isinstance(exports, dict):
|
|
322
|
+
if any("plugin" in str(k).lower() for k in exports):
|
|
323
|
+
return True
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
def _criticality(self, role: str, fan_in: int) -> str:
|
|
327
|
+
if role in {"runtime_core", "plugin_host"}:
|
|
328
|
+
return "high"
|
|
329
|
+
if role in {"backend_runtime", "frontend_runtime", "composition_layer"} or fan_in >= 3:
|
|
330
|
+
return "medium"
|
|
331
|
+
return "low"
|
sourcecode/schema.py
CHANGED
|
@@ -72,6 +72,7 @@ class EntryPoint:
|
|
|
72
72
|
confidence: Literal["high", "medium", "low"] = "high"
|
|
73
73
|
reason: Optional[str] = None # console_script | entry_file_pattern | main_guard | typer_app | heuristic | convention
|
|
74
74
|
evidence: Optional[str] = None # brief evidence string
|
|
75
|
+
entrypoint_type: Optional[Literal["production", "development", "benchmark", "example"]] = None
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
@dataclass
|
|
@@ -307,6 +308,36 @@ class SemanticSummary:
|
|
|
307
308
|
limitations: list[str] = field(default_factory=list)
|
|
308
309
|
|
|
309
310
|
|
|
311
|
+
# --- Runtime Architecture ---
|
|
312
|
+
|
|
313
|
+
ArchitecturalRole = Literal[
|
|
314
|
+
"runtime_core",
|
|
315
|
+
"plugin_host",
|
|
316
|
+
"plugin_package",
|
|
317
|
+
"frontend_runtime",
|
|
318
|
+
"backend_runtime",
|
|
319
|
+
"composition_layer",
|
|
320
|
+
"infrastructure_layer",
|
|
321
|
+
"tooling_layer",
|
|
322
|
+
"docs_layer",
|
|
323
|
+
"test_layer",
|
|
324
|
+
"benchmark_layer",
|
|
325
|
+
"unknown",
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@dataclass
|
|
330
|
+
class MonorepoPackageInfo:
|
|
331
|
+
"""Classified workspace package with architectural role and criticality."""
|
|
332
|
+
|
|
333
|
+
path: str
|
|
334
|
+
name: str
|
|
335
|
+
architectural_role: str # ArchitecturalRole value
|
|
336
|
+
criticality: Literal["high", "medium", "low"] = "low"
|
|
337
|
+
confidence: Literal["high", "medium", "low"] = "low"
|
|
338
|
+
role_signals: list[str] = field(default_factory=list)
|
|
339
|
+
|
|
340
|
+
|
|
310
341
|
# --- Phase 13 Plan 04: Architectural Inference ---
|
|
311
342
|
|
|
312
343
|
@dataclass
|
|
@@ -547,8 +578,10 @@ class SourceMap:
|
|
|
547
578
|
code_notes: list[CodeNote] = field(default_factory=list)
|
|
548
579
|
code_adrs: list[AdrRecord] = field(default_factory=list)
|
|
549
580
|
code_notes_summary: Optional[CodeNotesSummary] = None
|
|
550
|
-
# Confidence & Explainability (v0.
|
|
581
|
+
# Confidence & Explainability (v0.26.0)
|
|
551
582
|
confidence_summary: Optional[ConfidenceSummary] = None
|
|
552
583
|
analysis_gaps: list[AnalysisGap] = field(default_factory=list)
|
|
553
|
-
# AI context summary (v0.
|
|
584
|
+
# AI context summary (v0.26.0)
|
|
554
585
|
context_summary: Optional[ContextSummary] = None
|
|
586
|
+
# Runtime architecture (v0.26.0)
|
|
587
|
+
monorepo_packages: list[MonorepoPackageInfo] = field(default_factory=list)
|
sourcecode/serializer.py
CHANGED
|
@@ -410,18 +410,48 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
|
|
|
410
410
|
|
|
411
411
|
result: dict[str, Any] = {"project": project}
|
|
412
412
|
|
|
413
|
-
# ── 2. Entry points
|
|
413
|
+
# ── 2. Entry points: production/runtime first, benchmark/example excluded ──
|
|
414
414
|
if sm.entry_points:
|
|
415
415
|
_ep_skip = {"workspace"}
|
|
416
|
-
|
|
416
|
+
_aux_parts = frozenset({
|
|
417
|
+
"benchmark", "benchmarks", "bench", "demo", "demos",
|
|
418
|
+
"example", "examples", "docs", "doc", "fixtures", "fixture",
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
def _ep_priority(ep_dict: dict[str, Any]) -> int:
|
|
422
|
+
ep_type = ep_dict.get("entrypoint_type")
|
|
423
|
+
if ep_type in ("benchmark", "example"):
|
|
424
|
+
return 10
|
|
425
|
+
path_parts = ep_dict.get("path", "").replace("\\", "/").lower().split("/")
|
|
426
|
+
if any(p in _aux_parts for p in path_parts):
|
|
427
|
+
return 5
|
|
428
|
+
if ep_type == "development":
|
|
429
|
+
return 3
|
|
430
|
+
return 0
|
|
431
|
+
|
|
432
|
+
all_ep = [
|
|
417
433
|
{k: v for k, v in asdict(ep).items() if v is not None and v != "" and k not in _ep_skip}
|
|
418
434
|
for ep in sm.entry_points
|
|
419
435
|
]
|
|
436
|
+
all_ep.sort(key=_ep_priority)
|
|
437
|
+
operational_ep = [ep for ep in all_ep if _ep_priority(ep) < 5]
|
|
438
|
+
result["entry_points"] = operational_ep if operational_ep else all_ep
|
|
420
439
|
|
|
421
440
|
# ── 3. Architecture ───────────────────────────────────────────────────────
|
|
422
441
|
if sm.architecture_summary:
|
|
423
442
|
result["architecture"] = sm.architecture_summary
|
|
424
443
|
|
|
444
|
+
# ── 3b. Monorepo package roles (when available) ───────────────────────────
|
|
445
|
+
if sm.monorepo_packages:
|
|
446
|
+
_noise_roles = {"benchmark_layer", "tooling_layer", "docs_layer", "test_layer"}
|
|
447
|
+
operational_pkgs = [
|
|
448
|
+
{"path": p.path, "role": p.architectural_role, "criticality": p.criticality}
|
|
449
|
+
for p in sm.monorepo_packages
|
|
450
|
+
if p.architectural_role not in _noise_roles
|
|
451
|
+
]
|
|
452
|
+
if operational_pkgs:
|
|
453
|
+
result["runtime_packages"] = operational_pkgs
|
|
454
|
+
|
|
425
455
|
# ── 4. Key dependencies (role-sorted, already computed) ───────────────────
|
|
426
456
|
if sm.dependency_summary and sm.dependency_summary.requested and sm.key_dependencies:
|
|
427
457
|
_dep_skip = {"parent", "manifest_path", "workspace", "source", "ecosystem"}
|
sourcecode/summarizer.py
CHANGED
|
@@ -6,9 +6,10 @@ Sin llamadas a API — solo templates aplicados sobre SourceMap.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
9
10
|
|
|
10
11
|
from sourcecode.detectors.parsers import load_json_file, load_toml_file
|
|
11
|
-
from sourcecode.schema import SourceMap
|
|
12
|
+
from sourcecode.schema import MonorepoPackageInfo, SourceMap
|
|
12
13
|
|
|
13
14
|
_TOOLING_PREFIXES = (".claude/", ".vscode/", "bin/")
|
|
14
15
|
_SRC_TRANSPARENT = {"src", "lib", "app", "pkg"}
|
|
@@ -75,6 +76,11 @@ class ProjectSummarizer:
|
|
|
75
76
|
return "Proyecto analizado."
|
|
76
77
|
|
|
77
78
|
def _build_summary(self, sm: SourceMap) -> str:
|
|
79
|
+
# For monorepos with classified packages, structural inference beats README
|
|
80
|
+
if sm.project_type == "monorepo" and sm.monorepo_packages:
|
|
81
|
+
dep_part = self._build_dep_part(sm)
|
|
82
|
+
return self._build_monorepo_structural_summary(sm, dep_part)
|
|
83
|
+
|
|
78
84
|
description = self._read_project_description()
|
|
79
85
|
if description:
|
|
80
86
|
return self._merge_description_with_structure(description, sm)
|
|
@@ -107,6 +113,9 @@ class ProjectSummarizer:
|
|
|
107
113
|
dep_part = self._build_dep_part(sm)
|
|
108
114
|
|
|
109
115
|
if project_type == "monorepo":
|
|
116
|
+
# Prefer structural inference over README for monorepos
|
|
117
|
+
if sm.monorepo_packages:
|
|
118
|
+
return self._build_monorepo_structural_summary(sm, dep_part)
|
|
110
119
|
stacks_desc = ", ".join(sorted({s.stack.capitalize() for s in non_tooling_stacks}))
|
|
111
120
|
n_ws = len({s.workspace for s in non_tooling_stacks if s.workspace})
|
|
112
121
|
ws_part = f" con {n_ws} workspaces" if n_ws > 0 else ""
|
|
@@ -266,6 +275,71 @@ class ProjectSummarizer:
|
|
|
266
275
|
sorted_domains = sorted(domain_counts.items(), key=lambda x: x[1], reverse=True)
|
|
267
276
|
return [name for name, _ in sorted_domains[:5]]
|
|
268
277
|
|
|
278
|
+
def _build_monorepo_structural_summary(self, sm: SourceMap, dep_part: str) -> str:
|
|
279
|
+
pkgs = sm.monorepo_packages
|
|
280
|
+
total = len(pkgs)
|
|
281
|
+
|
|
282
|
+
# Group by role
|
|
283
|
+
by_role: dict[str, list[MonorepoPackageInfo]] = {}
|
|
284
|
+
for p in pkgs:
|
|
285
|
+
by_role.setdefault(p.architectural_role, []).append(p)
|
|
286
|
+
|
|
287
|
+
runtime_roles = {"runtime_core", "backend_runtime", "plugin_host"}
|
|
288
|
+
frontend_roles = {"frontend_runtime"}
|
|
289
|
+
plugin_roles = {"plugin_package"}
|
|
290
|
+
noise_roles = {"benchmark_layer", "tooling_layer", "docs_layer", "test_layer"}
|
|
291
|
+
|
|
292
|
+
runtime_pkgs = [p for r in runtime_roles for p in by_role.get(r, [])]
|
|
293
|
+
frontend_pkgs = [p for r in frontend_roles for p in by_role.get(r, [])]
|
|
294
|
+
plugin_pkgs = by_role.get("plugin_package", [])
|
|
295
|
+
composition_pkgs = by_role.get("composition_layer", [])
|
|
296
|
+
noise_count = sum(len(by_role.get(r, [])) for r in noise_roles)
|
|
297
|
+
|
|
298
|
+
# Detect plugin system
|
|
299
|
+
has_plugin_system = bool(plugin_pkgs) or bool(by_role.get("plugin_host"))
|
|
300
|
+
|
|
301
|
+
# Determine primary language
|
|
302
|
+
non_tooling = [s for s in sm.stacks if not self._is_tooling_path(s.root) and not self._is_tooling_path(s.workspace)]
|
|
303
|
+
lang_set = sorted({s.stack.capitalize() for s in non_tooling})
|
|
304
|
+
lang_desc = "/".join(lang_set[:2]) if lang_set else "TypeScript"
|
|
305
|
+
|
|
306
|
+
parts: list[str] = []
|
|
307
|
+
|
|
308
|
+
# Headline: type + language + key architecture signal
|
|
309
|
+
if has_plugin_system:
|
|
310
|
+
parts.append(f"Plugin-driven {lang_desc} monorepo with {total} packages.")
|
|
311
|
+
else:
|
|
312
|
+
ws_label = f"{total} packages" if total > 1 else "monorepo"
|
|
313
|
+
parts.append(f"{lang_desc} monorepo ({ws_label}).")
|
|
314
|
+
|
|
315
|
+
# Runtime core
|
|
316
|
+
if runtime_pkgs:
|
|
317
|
+
core_paths = ", ".join(p.path for p in runtime_pkgs[:4])
|
|
318
|
+
parts.append(f"Runtime core: {core_paths}.")
|
|
319
|
+
|
|
320
|
+
# Plugin packages (if plugin system detected)
|
|
321
|
+
if plugin_pkgs:
|
|
322
|
+
n_plugins = len(plugin_pkgs)
|
|
323
|
+
plugin_paths = ", ".join(p.path for p in plugin_pkgs[:3])
|
|
324
|
+
extra = f" (+ {n_plugins - 3} more)" if n_plugins > 3 else ""
|
|
325
|
+
parts.append(f"{n_plugins} plugin packages: {plugin_paths}{extra}.")
|
|
326
|
+
|
|
327
|
+
# Frontend
|
|
328
|
+
if frontend_pkgs:
|
|
329
|
+
fe_paths = ", ".join(p.path for p in frontend_pkgs[:3])
|
|
330
|
+
parts.append(f"Frontend runtime: {fe_paths}.")
|
|
331
|
+
|
|
332
|
+
# Composition/presets
|
|
333
|
+
if composition_pkgs:
|
|
334
|
+
comp_paths = ", ".join(p.path for p in composition_pkgs[:2])
|
|
335
|
+
parts.append(f"Composition layer: {comp_paths}.")
|
|
336
|
+
|
|
337
|
+
# Noise info (non-critical)
|
|
338
|
+
if noise_count > 0:
|
|
339
|
+
parts.append(f"{noise_count} auxiliary packages (tests/benchmarks/tooling — non-critical).")
|
|
340
|
+
|
|
341
|
+
return " ".join(parts) + dep_part
|
|
342
|
+
|
|
269
343
|
def _build_dep_part(self, sm: SourceMap) -> str:
|
|
270
344
|
if sm.dependency_summary and sm.dependency_summary.total_count > 0:
|
|
271
345
|
ds = sm.dependency_summary
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=KWokO203w00OE-dSv_Mo5TSB7XXds2ISCqZJXXjyH1k,100
|
|
2
2
|
sourcecode/architecture_analyzer.py,sha256=h81uQuSlxAorHve7M4jzXLT2PStOaGRMTlEPgW9zvxo,19741
|
|
3
|
-
sourcecode/architecture_summary.py,sha256=
|
|
3
|
+
sourcecode/architecture_summary.py,sha256=qolHmn6MWUIQHzY9WeHcfN41EJkQdnPQ5F_Z8pqQasA,20251
|
|
4
4
|
sourcecode/classifier.py,sha256=Ft_RfYS-KOe0t7vjgUx04OoCJd1-DXK7k9-I0CFDSnU,6934
|
|
5
|
-
sourcecode/cli.py,sha256
|
|
5
|
+
sourcecode/cli.py,sha256=-u4HmXhrJE5II5ohFbg_uVTlFXcMPhXf3RIl1YPLhIk,31278
|
|
6
6
|
sourcecode/code_notes_analyzer.py,sha256=rRd8bFYV0krjlxxQV0wenwE9K7pVpUQSR7KvSvUQKw4,9226
|
|
7
7
|
sourcecode/confidence_analyzer.py,sha256=gvQ92XCeGsACskSGU-GWTB7OqU1tT0eTgV-A7XA_lGs,8968
|
|
8
8
|
sourcecode/context_summarizer.py,sha256=CiQrfBEzun949bWvmLabWoj2HhPn6Lw62ofqnsy0FlQ,6503
|
|
@@ -11,15 +11,17 @@ sourcecode/dependency_analyzer.py,sha256=Exq0BfInvfS5iAg9xAr6WI2uPNuotkIudTKcYJc
|
|
|
11
11
|
sourcecode/doc_analyzer.py,sha256=Ec3orx6vBKsh5cNM3-F4y2Got2KuKx8w3dErwtdtM-A,19891
|
|
12
12
|
sourcecode/env_analyzer.py,sha256=JZxBOuIxnM0xw0IaJFL6LiPJBErL848L3XoTOHGBqZI,13554
|
|
13
13
|
sourcecode/git_analyzer.py,sha256=S9PGt8RDasBQYQUsZh9-H_KWVlPvzwR4jgM7TKN9ZCk,7643
|
|
14
|
-
sourcecode/graph_analyzer.py,sha256=
|
|
14
|
+
sourcecode/graph_analyzer.py,sha256=hMOsLLz9B0UnQ4xwbHdgr3bFvqpw0bQ8kN-xmEn3Krk,64156
|
|
15
15
|
sourcecode/metrics_analyzer.py,sha256=4uh11v-Q0gdrN87BOxuFWUym3N3AOkOuy21K5N8peB8,20126
|
|
16
|
-
sourcecode/prepare_context.py,sha256
|
|
16
|
+
sourcecode/prepare_context.py,sha256=--lD2dhNkBYI8kwb14d1DlFmEN8XF1Ygtf0Qk7-Y1Bs,30911
|
|
17
17
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
18
|
+
sourcecode/relevance_scorer.py,sha256=2yvxDFnz9YGrHEJubgx9soiVIDZHKv_pntOtTARtKow,5928
|
|
19
|
+
sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBGPjQ,14792
|
|
18
20
|
sourcecode/scanner.py,sha256=aM3h9-DCQ3xKpeHpHYdo2vX6T5P95HA_YwZbkAVNwmo,8288
|
|
19
|
-
sourcecode/schema.py,sha256=
|
|
21
|
+
sourcecode/schema.py,sha256=c2ADsc0MDtsCJ6TIFqEfjhkCsSeFhIY0XhXc8BqfrrQ,19208
|
|
20
22
|
sourcecode/semantic_analyzer.py,sha256=asQfJf-EhzYaOTA-iMuZsrVXtbW7SV2WEKCxgsxa88Y,79413
|
|
21
|
-
sourcecode/serializer.py,sha256=
|
|
22
|
-
sourcecode/summarizer.py,sha256=
|
|
23
|
+
sourcecode/serializer.py,sha256=k8ffuXNkjkBSU8RNad3lv_89FdRbJ7qdEmhD988X_Sw,26756
|
|
24
|
+
sourcecode/summarizer.py,sha256=YfBixsN1zWHHXdOEqaf793BylbJrsj75ST7FN6jcqRU,15424
|
|
23
25
|
sourcecode/tree_utils.py,sha256=Fj9OIuUksBvgibNd3feog0sMDjVypJzPexp5lvMoYWI,1424
|
|
24
26
|
sourcecode/workspace.py,sha256=fQlVoNx8S-fSHpKoJ0JBvEHCFkxszH0KZVJed1i3TRk,6845
|
|
25
27
|
sourcecode/detectors/__init__.py,sha256=A0AACJFF6HWf_RgatNtWu3PUzstcKtIGM9f1PoFcJug,1987
|
|
@@ -33,7 +35,7 @@ sourcecode/detectors/heuristic.py,sha256=JWb_dXNI1YDxk9a3DFeXTbKYfAGOFqV6jVxxqou
|
|
|
33
35
|
sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
|
|
34
36
|
sourcecode/detectors/java.py,sha256=cZvB13cqJ76zHDncEG-TOCuK8gJjJN2mZGS2DGEcZy8,7715
|
|
35
37
|
sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
|
|
36
|
-
sourcecode/detectors/nodejs.py,sha256=
|
|
38
|
+
sourcecode/detectors/nodejs.py,sha256=BOGaghCC0nAZRvsib9Tptvr3-dJOzASnDPicXHez80c,10550
|
|
37
39
|
sourcecode/detectors/parsers.py,sha256=ugPg8yNUf0Ai1gA7Fnn6wAkYGFjTxRodSP3IeViYJJ4,2290
|
|
38
40
|
sourcecode/detectors/php.py,sha256=W_AQD0WMVDdWHa9h_ilX6W8XSpz0X4ctpMK2WXfXf1I,1887
|
|
39
41
|
sourcecode/detectors/project.py,sha256=YCynNC8drJutPMiDGPutlYaL49IO8M-1Ms_RwVY09ss,6221
|
|
@@ -43,7 +45,7 @@ sourcecode/detectors/rust.py,sha256=Tij1vz8BFZ332GEvVkL6vyMli2OMHJfHyDAppWfe66c,
|
|
|
43
45
|
sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5TcqQ,1627
|
|
44
46
|
sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
|
|
45
47
|
sourcecode/detectors/tooling.py,sha256=hIvop80No22pqyGVJ32NKliSdjkHRePQkIRroqG01bY,1875
|
|
46
|
-
sourcecode-0.
|
|
47
|
-
sourcecode-0.
|
|
48
|
-
sourcecode-0.
|
|
49
|
-
sourcecode-0.
|
|
48
|
+
sourcecode-0.26.0.dist-info/METADATA,sha256=5OCyy0oIIzulSBOou5ZDGiNrcggn_VT3FNxly1gu33s,30326
|
|
49
|
+
sourcecode-0.26.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
50
|
+
sourcecode-0.26.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
|
|
51
|
+
sourcecode-0.26.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|