sourcecode 0.25.0__tar.gz → 0.26.0__tar.gz
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-0.25.0 → sourcecode-0.26.0}/.gitignore +3 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/PKG-INFO +1 -1
- {sourcecode-0.25.0 → sourcecode-0.26.0}/docs/schema.md +2 -2
- {sourcecode-0.25.0 → sourcecode-0.26.0}/pyproject.toml +1 -1
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/architecture_summary.py +40 -2
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/cli.py +8 -0
- sourcecode-0.26.0/src/sourcecode/detectors/nodejs.py +265 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/graph_analyzer.py +50 -1
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/prepare_context.py +73 -4
- sourcecode-0.26.0/src/sourcecode/relevance_scorer.py +182 -0
- sourcecode-0.26.0/src/sourcecode/runtime_classifier.py +331 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/schema.py +35 -2
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/serializer.py +32 -2
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/summarizer.py +75 -1
- sourcecode-0.26.0/tests/__init__.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_dependency_analyzer_node_python.py +1 -1
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_detector_nodejs.py +3 -2
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_hybrid_inference.py +1 -1
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_integration.py +1 -1
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_integration_dependencies.py +1 -1
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_schema.py +2 -2
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_signal_hierarchy.py +1 -1
- sourcecode-0.25.0/src/sourcecode/detectors/nodejs.py +0 -175
- {sourcecode-0.25.0 → sourcecode-0.26.0}/.ruff.toml +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/README.md +0 -0
- /sourcecode-0.25.0/tests/__init__.py → /sourcecode-0.26.0/raw +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/conftest.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_architecture_summary.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_classifier.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_cli.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_detector_dotnet.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_detector_python.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_detectors_base.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_graph_analyzer_polyglot.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_integration_docs.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_integration_graph_modules.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_integration_lqn.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_integration_metrics.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_packaging.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_phase1_improvements.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_real_projects.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_redactor.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_scanner.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_summarizer.py +0 -0
- {sourcecode-0.25.0 → sourcecode-0.26.0}/tests/test_workspace_analyzer.py +0 -0
|
@@ -81,7 +81,7 @@ Campos:
|
|
|
81
81
|
{
|
|
82
82
|
"schema_version": "1.0",
|
|
83
83
|
"generated_at": "2026-04-07T19:41:05.686277+00:00",
|
|
84
|
-
"sourcecode_version": "0.
|
|
84
|
+
"sourcecode_version": "0.26.0",
|
|
85
85
|
"analyzed_path": "/abs/path/to/project"
|
|
86
86
|
}
|
|
87
87
|
```
|
|
@@ -645,7 +645,7 @@ Ejemplo de salida para un monorepo con web Node.js y API Python con `--dependenc
|
|
|
645
645
|
"metadata": {
|
|
646
646
|
"schema_version": "1.0",
|
|
647
647
|
"generated_at": "2026-04-07T19:41:05.686277+00:00",
|
|
648
|
-
"sourcecode_version": "0.
|
|
648
|
+
"sourcecode_version": "0.26.0",
|
|
649
649
|
"analyzed_path": "/abs/path/to/project"
|
|
650
650
|
},
|
|
651
651
|
"file_tree": {
|
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from sourcecode.detectors.base import (
|
|
6
|
+
AbstractDetector,
|
|
7
|
+
DetectionContext,
|
|
8
|
+
EntryPoint,
|
|
9
|
+
StackDetection,
|
|
10
|
+
)
|
|
11
|
+
from sourcecode.detectors.parsers import load_json_file, unique_strings
|
|
12
|
+
from sourcecode.schema import FrameworkDetection
|
|
13
|
+
from sourcecode.tree_utils import path_exists_in_tree
|
|
14
|
+
|
|
15
|
+
_FRAMEWORK_MAP = {
|
|
16
|
+
"next": "Next.js",
|
|
17
|
+
"express": "Express",
|
|
18
|
+
"react": "React",
|
|
19
|
+
"vite": "Vite",
|
|
20
|
+
"vue": "Vue",
|
|
21
|
+
"svelte": "Svelte",
|
|
22
|
+
"@nestjs/core": "NestJS",
|
|
23
|
+
"fastify": "Fastify",
|
|
24
|
+
"hono": "Hono",
|
|
25
|
+
"@remix-run/node": "Remix",
|
|
26
|
+
"@remix-run/react": "Remix",
|
|
27
|
+
"astro": "Astro",
|
|
28
|
+
"nuxt": "Nuxt",
|
|
29
|
+
"@nuxt/kit": "Nuxt",
|
|
30
|
+
"gatsby": "Gatsby",
|
|
31
|
+
"@angular/core": "Angular",
|
|
32
|
+
"solid-js": "SolidJS",
|
|
33
|
+
"@trpc/server": "tRPC",
|
|
34
|
+
"graphql": "GraphQL",
|
|
35
|
+
"@apollo/server": "Apollo",
|
|
36
|
+
"apollo-server": "Apollo",
|
|
37
|
+
"koa": "Koa",
|
|
38
|
+
"elysia": "Elysia",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NodejsDetector(AbstractDetector):
|
|
43
|
+
name = "nodejs"
|
|
44
|
+
priority = 20
|
|
45
|
+
|
|
46
|
+
def can_detect(self, context: DetectionContext) -> bool:
|
|
47
|
+
if "package.json" not in context.manifests:
|
|
48
|
+
return False
|
|
49
|
+
if not path_exists_in_tree(context.file_tree, "package.json"):
|
|
50
|
+
return False
|
|
51
|
+
manifest_type = context.manifest_types.get("package.json", "application")
|
|
52
|
+
return manifest_type not in {"auxiliary", "config"}
|
|
53
|
+
|
|
54
|
+
def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
|
|
55
|
+
package_json = load_json_file(context.root / "package.json")
|
|
56
|
+
if package_json is None:
|
|
57
|
+
return [], []
|
|
58
|
+
|
|
59
|
+
from sourcecode.detectors.hybrid import merge_framework_detections, scan_for_frameworks
|
|
60
|
+
|
|
61
|
+
dependency_names = self._collect_dependency_names(package_json)
|
|
62
|
+
seen_fw: set[str] = set()
|
|
63
|
+
manifest_frameworks = []
|
|
64
|
+
for pkg_name, label in _FRAMEWORK_MAP.items():
|
|
65
|
+
if pkg_name in dependency_names and label not in seen_fw:
|
|
66
|
+
seen_fw.add(label)
|
|
67
|
+
manifest_frameworks.append(FrameworkDetection(name=label, source="package.json"))
|
|
68
|
+
|
|
69
|
+
package_manager = self._detect_package_manager(context)
|
|
70
|
+
entry_points = self._collect_entry_points(context, package_json)
|
|
71
|
+
priority = [ep.path for ep in entry_points]
|
|
72
|
+
import_frameworks = scan_for_frameworks(context.root, context.file_tree, "nodejs", priority_paths=priority)
|
|
73
|
+
frameworks = merge_framework_detections(manifest_frameworks, import_frameworks)
|
|
74
|
+
signals = self._detect_monorepo_signals(context, package_json)
|
|
75
|
+
|
|
76
|
+
stack = StackDetection(
|
|
77
|
+
stack="nodejs",
|
|
78
|
+
detection_method="manifest",
|
|
79
|
+
confidence="high",
|
|
80
|
+
frameworks=frameworks,
|
|
81
|
+
package_manager=package_manager,
|
|
82
|
+
manifests=["package.json"],
|
|
83
|
+
signals=signals,
|
|
84
|
+
)
|
|
85
|
+
return [stack], entry_points
|
|
86
|
+
|
|
87
|
+
def _detect_monorepo_signals(
|
|
88
|
+
self, context: DetectionContext, package_json: dict[str, Any]
|
|
89
|
+
) -> list[str]:
|
|
90
|
+
signals: list[str] = []
|
|
91
|
+
if path_exists_in_tree(context.file_tree, "turbo.json"):
|
|
92
|
+
signals.append("monorepo:turbo")
|
|
93
|
+
elif path_exists_in_tree(context.file_tree, "nx.json"):
|
|
94
|
+
signals.append("monorepo:nx")
|
|
95
|
+
elif path_exists_in_tree(context.file_tree, "pnpm-workspace.yaml"):
|
|
96
|
+
signals.append("monorepo:pnpm")
|
|
97
|
+
elif isinstance(package_json.get("workspaces"), (list, dict)):
|
|
98
|
+
signals.append("monorepo:npm-workspaces")
|
|
99
|
+
return signals
|
|
100
|
+
|
|
101
|
+
def _collect_dependency_names(self, package_json: dict[str, Any]) -> set[str]:
|
|
102
|
+
names: set[str] = set()
|
|
103
|
+
for field in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
|
|
104
|
+
raw = package_json.get(field, {})
|
|
105
|
+
if isinstance(raw, dict):
|
|
106
|
+
names.update(str(name) for name in raw)
|
|
107
|
+
return names
|
|
108
|
+
|
|
109
|
+
def _detect_package_manager(self, context: DetectionContext) -> str | None:
|
|
110
|
+
for filename, package_manager in (
|
|
111
|
+
("bun.lockb", "bun"),
|
|
112
|
+
("pnpm-lock.yaml", "pnpm"),
|
|
113
|
+
("package-lock.json", "npm"),
|
|
114
|
+
("yarn.lock", "yarn"),
|
|
115
|
+
):
|
|
116
|
+
if path_exists_in_tree(context.file_tree, filename):
|
|
117
|
+
return package_manager
|
|
118
|
+
return None
|
|
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
|
+
|
|
130
|
+
def _collect_entry_points(
|
|
131
|
+
self, context: DetectionContext, package_json: dict[str, Any]
|
|
132
|
+
) -> list[EntryPoint]:
|
|
133
|
+
entry_points: list[EntryPoint] = []
|
|
134
|
+
seen: set[str] = set()
|
|
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
|
|
162
|
+
bin_field = package_json.get("bin")
|
|
163
|
+
bin_paths: list[str] = []
|
|
164
|
+
if isinstance(bin_field, str) and bin_field.strip():
|
|
165
|
+
bin_paths.append(bin_field.strip())
|
|
166
|
+
elif isinstance(bin_field, dict):
|
|
167
|
+
bin_paths.extend(
|
|
168
|
+
str(v).strip() for v in bin_field.values()
|
|
169
|
+
if isinstance(v, str) and v.strip()
|
|
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
|
+
))
|
|
184
|
+
|
|
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
|
+
))
|
|
199
|
+
|
|
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
|
+
]:
|
|
207
|
+
if path in seen or not path_exists_in_tree(context.file_tree, path):
|
|
208
|
+
continue
|
|
209
|
+
ep_type = self._path_entrypoint_type(path)
|
|
210
|
+
kind = "web" if path.startswith(("app/", "pages/")) else "server"
|
|
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
|
+
|
|
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"
|
|
@@ -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
|
|
@@ -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(
|