sourcecode 1.30.27__py3-none-any.whl → 1.30.29__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/adaptive_scanner.py +11 -7
- sourcecode/classifier.py +8 -1
- sourcecode/cli.py +89 -7
- sourcecode/detectors/nodejs.py +1 -0
- sourcecode/prepare_context.py +109 -5
- sourcecode/repository_ir.py +90 -3
- sourcecode/scanner.py +25 -6
- sourcecode/serializer.py +121 -2
- {sourcecode-1.30.27.dist-info → sourcecode-1.30.29.dist-info}/METADATA +3 -3
- {sourcecode-1.30.27.dist-info → sourcecode-1.30.29.dist-info}/RECORD +14 -14
- {sourcecode-1.30.27.dist-info → sourcecode-1.30.29.dist-info}/WHEEL +0 -0
- {sourcecode-1.30.27.dist-info → sourcecode-1.30.29.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.30.27.dist-info → sourcecode-1.30.29.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/adaptive_scanner.py
CHANGED
|
@@ -19,7 +19,7 @@ from typing import Any, Optional, cast
|
|
|
19
19
|
from pathspec import GitIgnoreSpec
|
|
20
20
|
|
|
21
21
|
from sourcecode.repo_classifier import RepoTopology
|
|
22
|
-
from sourcecode.scanner import DEFAULT_EXCLUDES, MANIFEST_NAMES
|
|
22
|
+
from sourcecode.scanner import DEFAULT_EXCLUDES, EXCLUDED_FILE_SUFFIXES, MANIFEST_NAMES
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class AdaptiveScanner:
|
|
@@ -88,13 +88,14 @@ class AdaptiveScanner:
|
|
|
88
88
|
|
|
89
89
|
def _load_gitignore_spec(self) -> GitIgnoreSpec:
|
|
90
90
|
if self._gitignore_spec is None:
|
|
91
|
-
gitignore = self.root / ".gitignore"
|
|
92
91
|
lines: list[str] = []
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
for ignore_file in (".gitignore", ".sourcecodeIgnore"):
|
|
93
|
+
candidate = self.root / ignore_file
|
|
94
|
+
if candidate.exists():
|
|
95
|
+
try:
|
|
96
|
+
lines.extend(candidate.read_text(encoding="utf-8", errors="replace").splitlines())
|
|
97
|
+
except OSError:
|
|
98
|
+
pass
|
|
98
99
|
self._gitignore_spec = GitIgnoreSpec.from_lines(lines)
|
|
99
100
|
return self._gitignore_spec
|
|
100
101
|
|
|
@@ -202,6 +203,9 @@ class AdaptiveScanner:
|
|
|
202
203
|
# Skip flag-shaped names (shell redirect artifacts)
|
|
203
204
|
if fname.startswith("-"):
|
|
204
205
|
continue
|
|
206
|
+
# Skip minified/map files
|
|
207
|
+
if any(fname.endswith(sfx) for sfx in EXCLUDED_FILE_SUFFIXES):
|
|
208
|
+
continue
|
|
205
209
|
fpath = current / fname
|
|
206
210
|
if fpath.is_symlink():
|
|
207
211
|
continue
|
sourcecode/classifier.py
CHANGED
|
@@ -25,7 +25,7 @@ _API_FRAMEWORKS = {
|
|
|
25
25
|
"Ktor",
|
|
26
26
|
"Play",
|
|
27
27
|
}
|
|
28
|
-
_WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter", "Phoenix"}
|
|
28
|
+
_WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter", "Phoenix", "Angular"}
|
|
29
29
|
_CLI_FRAMEWORKS = {"Typer", "Cobra", "Clap"}
|
|
30
30
|
_API_STACKS = {"python", "go", "java", "php", "ruby", "dotnet", "kotlin", "scala"}
|
|
31
31
|
ConfidenceLevel = Literal["high", "medium", "low"]
|
|
@@ -105,6 +105,13 @@ class TypeClassifier:
|
|
|
105
105
|
if "src/lib.rs" in flat_paths and not any(path.endswith("main.rs") for path in flat_paths):
|
|
106
106
|
return "library"
|
|
107
107
|
|
|
108
|
+
# Angular SPA: angular.json is the canonical signal; framework detection is secondary
|
|
109
|
+
if (
|
|
110
|
+
"angular.json" in flat_paths
|
|
111
|
+
or "Angular" in framework_names
|
|
112
|
+
):
|
|
113
|
+
return "angular-spa"
|
|
114
|
+
|
|
108
115
|
if framework_names & _WEB_FRAMEWORKS or any(
|
|
109
116
|
path.startswith(("app/", "pages/", "components/")) for path in flat_paths
|
|
110
117
|
):
|
sourcecode/cli.py
CHANGED
|
@@ -424,7 +424,7 @@ def main(
|
|
|
424
424
|
no_redact: bool = typer.Option(
|
|
425
425
|
False,
|
|
426
426
|
"--no-redact",
|
|
427
|
-
help="Disable secret redaction.
|
|
427
|
+
help="Disable secret redaction of output strings. Note: env var values from the OS are never included in output regardless of this flag (security policy).",
|
|
428
428
|
),
|
|
429
429
|
version: Optional[bool] = typer.Option(
|
|
430
430
|
None,
|
|
@@ -437,7 +437,7 @@ def main(
|
|
|
437
437
|
depth: int = typer.Option(
|
|
438
438
|
4,
|
|
439
439
|
"--depth",
|
|
440
|
-
help="File tree traversal depth (default: 4). Java/Maven projects auto-adjust to 12.",
|
|
440
|
+
help="File tree traversal depth (default: 4). Java/Maven projects auto-adjust to a minimum of 12; values below 12 have no effect on Java projects.",
|
|
441
441
|
min=1,
|
|
442
442
|
max=20,
|
|
443
443
|
),
|
|
@@ -592,6 +592,16 @@ def main(
|
|
|
592
592
|
"-c",
|
|
593
593
|
help="Copy output to system clipboard after a successful run. No-op when --output is used or clipboard is unavailable.",
|
|
594
594
|
),
|
|
595
|
+
exclude: Optional[str] = typer.Option(
|
|
596
|
+
None,
|
|
597
|
+
"--exclude",
|
|
598
|
+
help="Additional directories/patterns to exclude, comma-separated (e.g. 'legacy,generated').",
|
|
599
|
+
),
|
|
600
|
+
no_cache: bool = typer.Option(
|
|
601
|
+
False,
|
|
602
|
+
"--no-cache",
|
|
603
|
+
help="Bypass the scan cache and force a fresh analysis.",
|
|
604
|
+
),
|
|
595
605
|
) -> None:
|
|
596
606
|
"""Analyze a repository and produce structured context for AI coding agents.
|
|
597
607
|
|
|
@@ -798,10 +808,59 @@ def main(
|
|
|
798
808
|
no_tree = True # agents never need the raw file tree
|
|
799
809
|
architecture = True # agents need full architectural signal (M4)
|
|
800
810
|
|
|
811
|
+
# ── GAP-9: Cache check — serve from .sourcecode-cache when git SHA unchanged ──
|
|
812
|
+
import hashlib as _hashlib
|
|
813
|
+
import subprocess as _sub
|
|
814
|
+
_cache_dir = target / ".sourcecode-cache"
|
|
815
|
+
_cache_hit_content: Optional[str] = None
|
|
816
|
+
_git_sha = ""
|
|
817
|
+
_cache_key = ""
|
|
818
|
+
_cache_file: Optional[Path] = None
|
|
819
|
+
if not no_cache:
|
|
820
|
+
try:
|
|
821
|
+
_sha_r = _sub.run(
|
|
822
|
+
["git", "-C", str(target), "rev-parse", "--short", "HEAD"],
|
|
823
|
+
capture_output=True, text=True, timeout=3,
|
|
824
|
+
)
|
|
825
|
+
_git_sha = _sha_r.stdout.strip()
|
|
826
|
+
# Only cache when target IS the git repo root (not a subdir of one),
|
|
827
|
+
# to avoid polluting sub-project directories used in tests.
|
|
828
|
+
if _git_sha and (target / ".git").exists():
|
|
829
|
+
# Include every output-affecting flag so different flag combos never collide
|
|
830
|
+
_flags_str = (
|
|
831
|
+
f"c={compact},ag={agent},fmt={format},full={full},"
|
|
832
|
+
f"co={changed_only},dep={dependencies},gm={graph_modules},"
|
|
833
|
+
f"docs={docs},fm={full_metrics},sem={semantics},"
|
|
834
|
+
f"arch={architecture},gc={git_context},em={env_map},"
|
|
835
|
+
f"cn={code_notes},tree={tree},mode={mode}"
|
|
836
|
+
)
|
|
837
|
+
_flags_h = _hashlib.md5(_flags_str.encode()).hexdigest()[:8]
|
|
838
|
+
_cache_key = f"{_git_sha}-{_flags_h}"
|
|
839
|
+
_cache_file = _cache_dir / f"snapshot-{_cache_key}.json"
|
|
840
|
+
if _cache_file.exists():
|
|
841
|
+
_cache_hit_content = _cache_file.read_text(encoding="utf-8")
|
|
842
|
+
except Exception:
|
|
843
|
+
_git_sha = ""
|
|
844
|
+
_cache_key = ""
|
|
845
|
+
_cache_file = None
|
|
846
|
+
|
|
847
|
+
if _cache_hit_content is not None:
|
|
848
|
+
from sourcecode.serializer import write_output
|
|
849
|
+
write_output(_cache_hit_content, output=output)
|
|
850
|
+
if copy and not output:
|
|
851
|
+
_copy_to_clipboard(_cache_hit_content)
|
|
852
|
+
return
|
|
853
|
+
|
|
854
|
+
# BUG-2: parse --exclude into extra_excludes frozenset
|
|
855
|
+
_extra_excludes: Optional[frozenset[str]] = None
|
|
856
|
+
if exclude:
|
|
857
|
+
_extra_excludes = frozenset(e.strip() for e in exclude.split(",") if e.strip())
|
|
858
|
+
|
|
801
859
|
_progress = Progress()
|
|
802
860
|
_progress.start("scanning files")
|
|
803
861
|
|
|
804
|
-
scanner = AdaptiveScanner(target, topology=_topology, base_depth=effective_depth
|
|
862
|
+
scanner = AdaptiveScanner(target, topology=_topology, base_depth=effective_depth,
|
|
863
|
+
extra_excludes=_extra_excludes)
|
|
805
864
|
raw_tree = scanner.scan_tree()
|
|
806
865
|
|
|
807
866
|
# 2. Filter .env and *.secret entries from file tree (SEC-02, all levels)
|
|
@@ -1514,9 +1573,10 @@ def main(
|
|
|
1514
1573
|
content = json.dumps(data, indent=2, ensure_ascii=False)
|
|
1515
1574
|
elif compact:
|
|
1516
1575
|
if changed_only and _allowed_changed_files:
|
|
1576
|
+
# GAP-5: preserve full entry_points for architecture context even in
|
|
1577
|
+
# --changed-only mode. Only filter file_paths and code_notes.
|
|
1517
1578
|
sm = _replace(sm,
|
|
1518
1579
|
file_paths=[p for p in sm.file_paths if p in _allowed_changed_files],
|
|
1519
|
-
entry_points=[ep for ep in sm.entry_points if ep.path in _allowed_changed_files],
|
|
1520
1580
|
code_notes=[n for n in sm.code_notes if n.path in _allowed_changed_files],
|
|
1521
1581
|
)
|
|
1522
1582
|
data = compact_view(sm, no_tree=no_tree, full=full)
|
|
@@ -1585,6 +1645,14 @@ def main(
|
|
|
1585
1645
|
_progress.finish()
|
|
1586
1646
|
write_output(content, output=output)
|
|
1587
1647
|
|
|
1648
|
+
# GAP-9: Persist to cache for future identical runs (git SHA unchanged)
|
|
1649
|
+
if not no_cache and _cache_key and _cache_file is not None and not _pipeline_error:
|
|
1650
|
+
try:
|
|
1651
|
+
_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
1652
|
+
_cache_file.write_text(content, encoding="utf-8")
|
|
1653
|
+
except Exception:
|
|
1654
|
+
pass
|
|
1655
|
+
|
|
1588
1656
|
if _pipeline_error:
|
|
1589
1657
|
raise typer.Exit(code=2)
|
|
1590
1658
|
|
|
@@ -1734,6 +1802,11 @@ def prepare_context_cmd(
|
|
|
1734
1802
|
help="Emit per-phase timing to stderr (git scan ms, symptom scoring ms, total ms)",
|
|
1735
1803
|
hidden=True,
|
|
1736
1804
|
),
|
|
1805
|
+
fast: bool = typer.Option(
|
|
1806
|
+
False,
|
|
1807
|
+
"--fast",
|
|
1808
|
+
help="Skip deep analysis (content search, test gap discovery, code annotations). Uses manifest/metadata only. Target: < 6 s.",
|
|
1809
|
+
),
|
|
1737
1810
|
) -> None:
|
|
1738
1811
|
"""Task-specific context for AI coding agents.
|
|
1739
1812
|
|
|
@@ -1808,9 +1881,14 @@ def prepare_context_cmd(
|
|
|
1808
1881
|
if since:
|
|
1809
1882
|
_phase += f" since {since}"
|
|
1810
1883
|
_progress.start(_phase)
|
|
1884
|
+
if not fast:
|
|
1885
|
+
import sys as _sys
|
|
1886
|
+
if _sys.stderr.isatty():
|
|
1887
|
+
_sys.stderr.write(f"Analyzing ({task})... (deep scan may take 15–35 s for large codebases)\n")
|
|
1888
|
+
_sys.stderr.flush()
|
|
1811
1889
|
_t0 = _time.perf_counter()
|
|
1812
1890
|
try:
|
|
1813
|
-
output = builder.build(task, since=since, symptom=symptom)
|
|
1891
|
+
output = builder.build(task, since=since, symptom=symptom, fast=fast)
|
|
1814
1892
|
finally:
|
|
1815
1893
|
_progress.finish()
|
|
1816
1894
|
_t_total = (_time.perf_counter() - _t0) * 1000
|
|
@@ -1960,8 +2038,8 @@ def prepare_context_cmd(
|
|
|
1960
2038
|
_sys.stdout.buffer.write(b"\n")
|
|
1961
2039
|
_sys.stdout.buffer.flush()
|
|
1962
2040
|
if copy:
|
|
1963
|
-
_copy_to_clipboard(_nc_json)
|
|
1964
|
-
|
|
2041
|
+
if _copy_to_clipboard(_nc_json):
|
|
2042
|
+
typer.echo("✓ copied to clipboard", err=True)
|
|
1965
2043
|
raise typer.Exit()
|
|
1966
2044
|
if output.ci_decision:
|
|
1967
2045
|
out["ci_decision"] = output.ci_decision
|
|
@@ -2082,6 +2160,10 @@ def prepare_context_cmd(
|
|
|
2082
2160
|
out["symptom_note"] = output.symptom_note
|
|
2083
2161
|
if output.symptom_explain:
|
|
2084
2162
|
out["symptom_explain"] = output.symptom_explain
|
|
2163
|
+
if getattr(output, "symptom_hint", None):
|
|
2164
|
+
out["symptom_hint"] = output.symptom_hint
|
|
2165
|
+
if getattr(output, "warnings", None):
|
|
2166
|
+
out["warnings"] = output.warnings
|
|
2085
2167
|
if llm_prompt:
|
|
2086
2168
|
out["llm_prompt"] = builder.render_prompt(output)
|
|
2087
2169
|
|
sourcecode/detectors/nodejs.py
CHANGED
|
@@ -217,6 +217,7 @@ class NodejsDetector(AbstractDetector):
|
|
|
217
217
|
_INDEX_PATHS = {"src/index.js", "src/index.ts", "index.js", "index.ts"}
|
|
218
218
|
for path in [
|
|
219
219
|
"server.js", "server.ts",
|
|
220
|
+
"main.ts", "main.js", # Angular/root-level entry (e.g. Angular CLI projects)
|
|
220
221
|
"src/index.js", "src/index.ts",
|
|
221
222
|
"src/main.js", "src/main.ts", "src/main.tsx",
|
|
222
223
|
"app/page.tsx", "pages/index.js",
|
sourcecode/prepare_context.py
CHANGED
|
@@ -335,6 +335,7 @@ class TaskOutput:
|
|
|
335
335
|
related_notes: list[dict] = field(default_factory=list) # fix-bug + symptom only
|
|
336
336
|
symptom_note: Optional[str] = None # fix-bug: cross-layer synonym note
|
|
337
337
|
symptom_explain: Optional[dict] = None # fix-bug: structured evidence breakdown
|
|
338
|
+
symptom_hint: Optional[str] = None # fix-bug: redirect hint when term not found in this module
|
|
338
339
|
# delta-specific impact fields
|
|
339
340
|
impact_summary: Optional[str] = None
|
|
340
341
|
affected_modules: list[str] = field(default_factory=list)
|
|
@@ -348,6 +349,7 @@ class TaskOutput:
|
|
|
348
349
|
error_code: Optional[str] = None
|
|
349
350
|
error_message: Optional[str] = None
|
|
350
351
|
error_hints: list[str] = field(default_factory=list)
|
|
352
|
+
warnings: list[dict] = field(default_factory=list) # structured warnings (REF_NOT_FOUND, etc.)
|
|
351
353
|
# CI decision state machine — machine-decidable signal
|
|
352
354
|
ci_decision: Optional[str] = None # "no_changes" | "analysis_success" | "git_ref_error" | "no_git_repo"
|
|
353
355
|
# git baseline resolution metadata
|
|
@@ -602,6 +604,33 @@ _FRONTEND_SYMPTOM_MAP: dict[str, list[str]] = {
|
|
|
602
604
|
}
|
|
603
605
|
|
|
604
606
|
|
|
607
|
+
MAX_FILES_FAST = 2000 # above this threshold --fast uses git-index-only mode
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _count_files_bounded(root: "Path", limit: int = MAX_FILES_FAST + 1) -> int:
|
|
611
|
+
"""Count files under root, stopping early once limit reached (O(n) fast exit)."""
|
|
612
|
+
import os as _os
|
|
613
|
+
count = 0
|
|
614
|
+
for _, _, fnames in _os.walk(str(root)):
|
|
615
|
+
count += len(fnames)
|
|
616
|
+
if count >= limit:
|
|
617
|
+
return count
|
|
618
|
+
return count
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _git_changed_files_fast(root: "Path") -> list[str]:
|
|
622
|
+
"""Return files reported by git diff --name-only HEAD (for fast-mode scanning)."""
|
|
623
|
+
import subprocess as _sp
|
|
624
|
+
try:
|
|
625
|
+
r = _sp.run(
|
|
626
|
+
["git", "diff", "--name-only", "HEAD"],
|
|
627
|
+
capture_output=True, text=True, cwd=str(root), timeout=5,
|
|
628
|
+
)
|
|
629
|
+
return [f.strip() for f in r.stdout.splitlines() if f.strip()]
|
|
630
|
+
except Exception:
|
|
631
|
+
return []
|
|
632
|
+
|
|
633
|
+
|
|
605
634
|
def _build_analysis_scope(
|
|
606
635
|
*,
|
|
607
636
|
task_name: str,
|
|
@@ -683,7 +712,7 @@ class TaskContextBuilder:
|
|
|
683
712
|
def __init__(self, root: Path) -> None:
|
|
684
713
|
self.root = root
|
|
685
714
|
|
|
686
|
-
def build(self, task_name: str, *, since: Optional[str] = None, symptom: Optional[str] = None) -> TaskOutput:
|
|
715
|
+
def build(self, task_name: str, *, since: Optional[str] = None, symptom: Optional[str] = None, fast: bool = False) -> TaskOutput:
|
|
687
716
|
if task_name not in TASKS:
|
|
688
717
|
raise ValueError(
|
|
689
718
|
f"Unknown task '{task_name}'. Available: {', '.join(TASKS)}"
|
|
@@ -759,6 +788,21 @@ class TaskContextBuilder:
|
|
|
759
788
|
# for behavioral_impact reverse lookups without scanning the whole repo).
|
|
760
789
|
file_tree: dict = {}
|
|
761
790
|
all_paths = self._expand_scope_for_analysis(_pr_scope_files or [])
|
|
791
|
+
elif fast and _count_files_bounded(self.root) > MAX_FILES_FAST:
|
|
792
|
+
# Fast mode on large repo: git-index-only — only scan git-changed files.
|
|
793
|
+
# Skips full AdaptiveScanner traversal which takes 35s+ on 7k+ file repos.
|
|
794
|
+
_git_files = _git_changed_files_fast(self.root)
|
|
795
|
+
_git_files = [f for f in _git_files if (self.root / f).exists()]
|
|
796
|
+
if not _git_files:
|
|
797
|
+
# Fallback: use a shallow scan (depth 2) to get some context
|
|
798
|
+
scanner = AdaptiveScanner(self.root, base_depth=2)
|
|
799
|
+
file_tree = scanner.scan_tree()
|
|
800
|
+
manifests = scanner.find_manifests()
|
|
801
|
+
all_paths = [p.replace("\\", "/") for p in flatten_file_tree(file_tree)]
|
|
802
|
+
else:
|
|
803
|
+
file_tree = {}
|
|
804
|
+
all_paths = _git_files
|
|
805
|
+
# Keep manifests from shallow pre-scan
|
|
762
806
|
else:
|
|
763
807
|
_topology = RepoClassifier().classify(self.root)
|
|
764
808
|
_base_depth = 12 if _is_java else 6
|
|
@@ -880,7 +924,7 @@ class TaskContextBuilder:
|
|
|
880
924
|
improvement_opportunities: list[str] = []
|
|
881
925
|
cn_notes_for_ranking: list = []
|
|
882
926
|
|
|
883
|
-
if spec.enable_code_notes:
|
|
927
|
+
if spec.enable_code_notes and not fast:
|
|
884
928
|
from dataclasses import asdict
|
|
885
929
|
from sourcecode.code_notes_analyzer import CodeNotesAnalyzer
|
|
886
930
|
|
|
@@ -1317,6 +1361,7 @@ class TaskContextBuilder:
|
|
|
1317
1361
|
related_notes: list[dict] = []
|
|
1318
1362
|
symptom_note: Optional[str] = None
|
|
1319
1363
|
symptom_explain: Optional[dict] = None
|
|
1364
|
+
symptom_hint: Optional[str] = None
|
|
1320
1365
|
if task_name == "fix-bug" and symptom:
|
|
1321
1366
|
import re as _re
|
|
1322
1367
|
_camel_expanded = _re.sub(r'([a-z])([A-Z])', r'\1 \2', symptom)
|
|
@@ -1410,8 +1455,9 @@ class TaskContextBuilder:
|
|
|
1410
1455
|
_existing_paths.add(_p)
|
|
1411
1456
|
_sx_direct_path.append(_p)
|
|
1412
1457
|
|
|
1413
|
-
# Pass
|
|
1414
|
-
#
|
|
1458
|
+
# Pass 4b: grep-based injection for frontend→backend synonym terms.
|
|
1459
|
+
# Runs parallel grep for each backend term to find files not yet in
|
|
1460
|
+
# the candidate pool (e.g. AkitaBaseService containing setLoading).
|
|
1415
1461
|
_src_exts = frozenset({".java", ".py", ".ts", ".js", ".kt", ".go"})
|
|
1416
1462
|
_frontend_kws = [kw for kw in symptom_keywords if kw in _FRONTEND_SYMPTOM_MAP]
|
|
1417
1463
|
_backend_terms_set: list[str] = []
|
|
@@ -1421,6 +1467,40 @@ class TaskContextBuilder:
|
|
|
1421
1467
|
_bt.extend(_FRONTEND_SYMPTOM_MAP[_fkw])
|
|
1422
1468
|
_backend_terms_set = list(dict.fromkeys(_bt))
|
|
1423
1469
|
|
|
1470
|
+
if _backend_terms_set and not fast:
|
|
1471
|
+
import subprocess as _sp2
|
|
1472
|
+
_grepped: set[str] = set()
|
|
1473
|
+
for _term in _backend_terms_set[:6]:
|
|
1474
|
+
try:
|
|
1475
|
+
_gr = _sp2.run(
|
|
1476
|
+
["grep", "-r", "-l", "-i", _term,
|
|
1477
|
+
"--include=*.ts", "--include=*.java",
|
|
1478
|
+
str(self.root)],
|
|
1479
|
+
capture_output=True, text=True, timeout=4,
|
|
1480
|
+
)
|
|
1481
|
+
for _line in _gr.stdout.splitlines():
|
|
1482
|
+
try:
|
|
1483
|
+
_rel = str(Path(_line.strip()).relative_to(self.root)).replace("\\", "/")
|
|
1484
|
+
_grepped.add(_rel)
|
|
1485
|
+
except ValueError:
|
|
1486
|
+
pass
|
|
1487
|
+
except Exception:
|
|
1488
|
+
pass
|
|
1489
|
+
_existing_paths_now = {rf.path for rf in relevant_files}
|
|
1490
|
+
for _gf in sorted(_grepped):
|
|
1491
|
+
if _gf in _existing_paths_now:
|
|
1492
|
+
continue
|
|
1493
|
+
if Path(_gf).suffix.lower() not in _src_exts:
|
|
1494
|
+
continue
|
|
1495
|
+
relevant_files.append(RelevantFile(
|
|
1496
|
+
path=_gf,
|
|
1497
|
+
role="symptom_match",
|
|
1498
|
+
score=0.45,
|
|
1499
|
+
reason="content contains backend symptom term (grep)",
|
|
1500
|
+
why=f"grep injection: {', '.join(_backend_terms_set[:3])}",
|
|
1501
|
+
))
|
|
1502
|
+
_existing_paths_now.add(_gf)
|
|
1503
|
+
|
|
1424
1504
|
# Sort before content scan so top candidates get read first
|
|
1425
1505
|
relevant_files = sorted(relevant_files, key=lambda rf: -rf.score)
|
|
1426
1506
|
_CONTENT_SCAN_LIMIT = 80
|
|
@@ -1537,9 +1617,30 @@ class TaskContextBuilder:
|
|
|
1537
1617
|
),
|
|
1538
1618
|
}
|
|
1539
1619
|
|
|
1620
|
+
# BUG #4: LOW confidence + 0 content matches → clear suspected_areas,
|
|
1621
|
+
# emit actionable redirect instead of unrelated files.
|
|
1622
|
+
if _sx_confidence == "LOW" and not _sx_content:
|
|
1623
|
+
suspected_areas = []
|
|
1624
|
+
_is_fe_term = any(kw in _FRONTEND_SYMPTOM_MAP for kw in symptom_keywords)
|
|
1625
|
+
_root_name = self.root.name
|
|
1626
|
+
if _is_fe_term:
|
|
1627
|
+
_fe_redirect = (
|
|
1628
|
+
f"Term {symptom!r} not found in sources under {_root_name!r}. "
|
|
1629
|
+
f"This appears to be a frontend symptom. "
|
|
1630
|
+
f"Try: prepare-context fix-bug . --symptom {symptom!r} "
|
|
1631
|
+
f"(monorepo root) or target a frontend sub-project directly."
|
|
1632
|
+
)
|
|
1633
|
+
else:
|
|
1634
|
+
_fe_redirect = (
|
|
1635
|
+
f"Term {symptom!r} not found in sources under {_root_name!r}. "
|
|
1636
|
+
f"Verify the spelling or try a related term. "
|
|
1637
|
+
f"If this is a frontend symptom, run against the frontend sub-project."
|
|
1638
|
+
)
|
|
1639
|
+
symptom_hint = _fe_redirect
|
|
1640
|
+
|
|
1540
1641
|
# ── 7. Test gaps (generate-tests only) ────────────────────────────
|
|
1541
1642
|
test_gaps: list[str] = []
|
|
1542
|
-
if task_name == "generate-tests":
|
|
1643
|
+
if task_name == "generate-tests" and not fast:
|
|
1543
1644
|
def _normalize_test_stem(stem: str) -> str:
|
|
1544
1645
|
# Java: FooTest / FooTests → Foo; TestFoo → Foo
|
|
1545
1646
|
if stem.endswith("Tests"):
|
|
@@ -1683,6 +1784,8 @@ class TaskContextBuilder:
|
|
|
1683
1784
|
resolved_since_ref=_delta_baseline.get("resolved_ref") if task_name == "delta" else None,
|
|
1684
1785
|
resolution_path=_delta_baseline.get("resolution_path") if task_name == "delta" else None,
|
|
1685
1786
|
diff_validation_status=_delta_baseline.get("diff_validation_status") if task_name == "delta" else None,
|
|
1787
|
+
warnings=_delta_baseline.get("warnings", []) if task_name == "delta" else [],
|
|
1788
|
+
symptom_hint=symptom_hint if task_name == "fix-bug" else None,
|
|
1686
1789
|
)
|
|
1687
1790
|
|
|
1688
1791
|
def render_prompt(self, output: TaskOutput) -> str:
|
|
@@ -3261,6 +3364,7 @@ class TaskContextBuilder:
|
|
|
3261
3364
|
"resolution_path": "head_minus_1_fallback",
|
|
3262
3365
|
"diff_validation_status": "invalid_ref", # original ref unresolved
|
|
3263
3366
|
"error": False,
|
|
3367
|
+
"warnings": [{"code": "REF_NOT_FOUND", "ref": since, "resolved_to": "HEAD~1"}],
|
|
3264
3368
|
}
|
|
3265
3369
|
|
|
3266
3370
|
# All stages failed
|
sourcecode/repository_ir.py
CHANGED
|
@@ -1091,8 +1091,54 @@ def _build_evidence_bundles(
|
|
|
1091
1091
|
return bundles
|
|
1092
1092
|
|
|
1093
1093
|
|
|
1094
|
-
def
|
|
1095
|
-
"""
|
|
1094
|
+
def _common_package_prefix(fqns: list[str]) -> str:
|
|
1095
|
+
"""Longest common dot-separated package prefix across a list of FQNs."""
|
|
1096
|
+
if not fqns:
|
|
1097
|
+
return ""
|
|
1098
|
+
# Strip class/method suffix — keep only package parts (lowercase segments)
|
|
1099
|
+
def pkg_parts(fqn: str) -> list[str]:
|
|
1100
|
+
parts = fqn.split(".")
|
|
1101
|
+
# Drop trailing class/method names (PascalCase or after '#')
|
|
1102
|
+
result = []
|
|
1103
|
+
for p in parts:
|
|
1104
|
+
if "#" in p:
|
|
1105
|
+
break
|
|
1106
|
+
if p and p[0].isupper():
|
|
1107
|
+
break
|
|
1108
|
+
result.append(p)
|
|
1109
|
+
return result
|
|
1110
|
+
|
|
1111
|
+
segs = [pkg_parts(f) for f in fqns if pkg_parts(f)]
|
|
1112
|
+
if not segs:
|
|
1113
|
+
return fqns[0].rsplit(".", 1)[0] if "." in fqns[0] else fqns[0]
|
|
1114
|
+
common = segs[0]
|
|
1115
|
+
for s in segs[1:]:
|
|
1116
|
+
new_common = []
|
|
1117
|
+
for a, b in zip(common, s):
|
|
1118
|
+
if a == b:
|
|
1119
|
+
new_common.append(a)
|
|
1120
|
+
else:
|
|
1121
|
+
break
|
|
1122
|
+
common = new_common
|
|
1123
|
+
if not common:
|
|
1124
|
+
break
|
|
1125
|
+
return ".".join(common) if common else fqns[0].rsplit(".", 1)[0]
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def _subsystem_label(package_prefix: str) -> str:
|
|
1129
|
+
"""Derive short human label from package prefix (last meaningful segment)."""
|
|
1130
|
+
parts = [p for p in package_prefix.split(".") if p]
|
|
1131
|
+
# Skip generic top-level segments
|
|
1132
|
+
_SKIP = {"com", "org", "net", "io", "java", "javax"}
|
|
1133
|
+
meaningful = [p for p in parts if p not in _SKIP]
|
|
1134
|
+
return meaningful[-1] if meaningful else (parts[-1] if parts else package_prefix)
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def _detect_subsystems(all_fqns: list[str], relations: list[RelationEdge]) -> list[dict]:
|
|
1138
|
+
"""Connected components of the relation graph (Union-Find, graph-only).
|
|
1139
|
+
|
|
1140
|
+
Returns labeled subsystem objects instead of raw FQN arrays.
|
|
1141
|
+
"""
|
|
1096
1142
|
fqn_set = set(all_fqns)
|
|
1097
1143
|
parent: dict[str, str] = {fqn: fqn for fqn in all_fqns}
|
|
1098
1144
|
|
|
@@ -1117,7 +1163,23 @@ def _detect_subsystems(all_fqns: list[str], relations: list[RelationEdge]) -> li
|
|
|
1117
1163
|
root = find(fqn)
|
|
1118
1164
|
components.setdefault(root, []).append(fqn)
|
|
1119
1165
|
|
|
1120
|
-
|
|
1166
|
+
result: list[dict] = []
|
|
1167
|
+
for members in sorted(components.values()):
|
|
1168
|
+
members = sorted(members)
|
|
1169
|
+
pkg_prefix = _common_package_prefix(members)
|
|
1170
|
+
label = _subsystem_label(pkg_prefix)
|
|
1171
|
+
# Summary: first 3 short class names + total count
|
|
1172
|
+
short_names = [m.split(".")[-1].split("#")[0] for m in members[:3]]
|
|
1173
|
+
summary = ", ".join(short_names)
|
|
1174
|
+
if len(members) > 3:
|
|
1175
|
+
summary += f" ... ({len(members)} total)"
|
|
1176
|
+
result.append({
|
|
1177
|
+
"label": label,
|
|
1178
|
+
"package_prefix": pkg_prefix,
|
|
1179
|
+
"member_count": len(members),
|
|
1180
|
+
"summary": summary,
|
|
1181
|
+
})
|
|
1182
|
+
return result
|
|
1121
1183
|
|
|
1122
1184
|
|
|
1123
1185
|
_EDGE_REASON_TEMPLATES: dict[str, str] = {
|
|
@@ -1287,6 +1349,7 @@ def _assemble(
|
|
|
1287
1349
|
|
|
1288
1350
|
# Score per node: ir_weight × graph_centrality × diff_intensity × evidence_strength
|
|
1289
1351
|
# Unchanged nodes: diff_intensity=0 → score=0 (no diff signal)
|
|
1352
|
+
has_diff = bool(sorted_changed)
|
|
1290
1353
|
node_scores: dict[str, float] = {}
|
|
1291
1354
|
for sym in sorted_syms:
|
|
1292
1355
|
fqn = sym.symbol
|
|
@@ -1299,6 +1362,28 @@ def _assemble(
|
|
|
1299
1362
|
es = bundles[fqn].evidence_strength if fqn in bundles else 0.0
|
|
1300
1363
|
node_scores[fqn] = round(w * c * di * es, 4) if di > 0 else 0.0
|
|
1301
1364
|
|
|
1365
|
+
# No diff signal (no --since): fall back to call-graph centrality scores.
|
|
1366
|
+
# Avoids emitting all-zero scores which mislead agents into thinking the tool is broken.
|
|
1367
|
+
score_basis: str
|
|
1368
|
+
impact_note: Optional[str]
|
|
1369
|
+
if not has_diff and sorted_syms:
|
|
1370
|
+
for sym in sorted_syms:
|
|
1371
|
+
fqn = sym.symbol
|
|
1372
|
+
role = spring_role_map.get(fqn, "other")
|
|
1373
|
+
w = _IR_WEIGHTS.get(role, _IR_WEIGHT_DEFAULT)
|
|
1374
|
+
raw_c = in_deg.get(fqn, 0) + out_deg.get(fqn, 0) + bfs_reach.get(fqn, 0) * 0.1
|
|
1375
|
+
c = min(1.0, raw_c / max_raw)
|
|
1376
|
+
node_scores[fqn] = round(w * c, 4)
|
|
1377
|
+
score_basis = "call_graph_centrality"
|
|
1378
|
+
impact_note = "impact scores based on call-graph centrality (no --since provided)"
|
|
1379
|
+
elif has_diff:
|
|
1380
|
+
score_basis = "diff_impact"
|
|
1381
|
+
impact_note = None
|
|
1382
|
+
else:
|
|
1383
|
+
# No symbols at all — omit scores entirely rather than emit zeros
|
|
1384
|
+
score_basis = "none"
|
|
1385
|
+
impact_note = None
|
|
1386
|
+
|
|
1302
1387
|
# --- Analysis: classify changed symbols ---
|
|
1303
1388
|
dropped_fields: list[dict] = []
|
|
1304
1389
|
changed_entities_out: list[dict] = []
|
|
@@ -1435,6 +1520,8 @@ def _assemble(
|
|
|
1435
1520
|
},
|
|
1436
1521
|
"impact": {
|
|
1437
1522
|
"global_score": global_score,
|
|
1523
|
+
"score_basis": score_basis,
|
|
1524
|
+
**({"impact_note": impact_note} if impact_note else {}),
|
|
1438
1525
|
"ranked_nodes": ranked_nodes,
|
|
1439
1526
|
},
|
|
1440
1527
|
"subsystems": subsystems,
|
sourcecode/scanner.py
CHANGED
|
@@ -27,6 +27,22 @@ DEFAULT_EXCLUDES: frozenset[str] = frozenset({
|
|
|
27
27
|
"dist",
|
|
28
28
|
"build",
|
|
29
29
|
"target",
|
|
30
|
+
# Additional generated/IDE/tool dirs
|
|
31
|
+
".gradle",
|
|
32
|
+
".planning",
|
|
33
|
+
".idea",
|
|
34
|
+
"coverage",
|
|
35
|
+
".sourcecode-cache",
|
|
36
|
+
".mvn",
|
|
37
|
+
"generated",
|
|
38
|
+
"generated-sources",
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
# File suffixes excluded by default (minified/map files add noise, not signal)
|
|
42
|
+
EXCLUDED_FILE_SUFFIXES: frozenset[str] = frozenset({
|
|
43
|
+
".min.js",
|
|
44
|
+
".min.css",
|
|
45
|
+
".map",
|
|
30
46
|
})
|
|
31
47
|
|
|
32
48
|
# Nombres de ficheros de manifiesto conocidos (para find_manifests)
|
|
@@ -120,13 +136,13 @@ class FileScanner:
|
|
|
120
136
|
self._gitignore_spec: Optional[GitIgnoreSpec] = None
|
|
121
137
|
|
|
122
138
|
def _load_gitignore_spec(self) -> GitIgnoreSpec:
|
|
123
|
-
"""Carga .gitignore
|
|
139
|
+
"""Carga .gitignore y .sourcecodeIgnore como GitIgnoreSpec (SCAN-01)."""
|
|
124
140
|
if self._gitignore_spec is None:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
lines: list[str] = []
|
|
142
|
+
for ignore_file in (".gitignore", ".sourcecodeIgnore"):
|
|
143
|
+
candidate = self.root / ignore_file
|
|
144
|
+
if candidate.exists():
|
|
145
|
+
lines.extend(candidate.read_text(encoding="utf-8", errors="replace").splitlines())
|
|
130
146
|
self._gitignore_spec = GitIgnoreSpec.from_lines(lines)
|
|
131
147
|
return self._gitignore_spec
|
|
132
148
|
|
|
@@ -183,6 +199,9 @@ class FileScanner:
|
|
|
183
199
|
# No legitimate source file starts with "-".
|
|
184
200
|
if fname.startswith("-"):
|
|
185
201
|
continue
|
|
202
|
+
# Skip minified/map files — no signal value
|
|
203
|
+
if any(fname.endswith(sfx) for sfx in EXCLUDED_FILE_SUFFIXES):
|
|
204
|
+
continue
|
|
186
205
|
fpath = current / fname
|
|
187
206
|
# SCAN-03: no incluir symlinks de fichero
|
|
188
207
|
if fpath.is_symlink():
|
sourcecode/serializer.py
CHANGED
|
@@ -248,6 +248,16 @@ def _compact_git_context(sm: "SourceMap") -> "Optional[dict[str, Any]]":
|
|
|
248
248
|
for f, n in _fc.most_common(5)
|
|
249
249
|
]
|
|
250
250
|
ctx["hotspots_source"] = "recent_commits"
|
|
251
|
+
if gc.recent_commits:
|
|
252
|
+
ctx["recent_commits"] = [
|
|
253
|
+
{
|
|
254
|
+
"hash": c.hash[:8],
|
|
255
|
+
"message": (c.message or "")[:80],
|
|
256
|
+
"date": (c.date or "")[:10],
|
|
257
|
+
"author": c.author or "",
|
|
258
|
+
}
|
|
259
|
+
for c in gc.recent_commits[:5]
|
|
260
|
+
]
|
|
251
261
|
return ctx if ctx else None
|
|
252
262
|
|
|
253
263
|
|
|
@@ -553,6 +563,10 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
|
|
|
553
563
|
if security:
|
|
554
564
|
result["security"] = security
|
|
555
565
|
if controllers:
|
|
566
|
+
# Count unique files (classes) vs total entries (methods/endpoints)
|
|
567
|
+
controller_classes = len({c["path"] for c in controllers})
|
|
568
|
+
controller_methods = len(controllers)
|
|
569
|
+
|
|
556
570
|
# Extract all DDD module names from controller paths and group by domain area.
|
|
557
571
|
# Path pattern: .../ddd/{module}/infrastructure/rest/*Controller.java
|
|
558
572
|
_DDD_LAYERS = {"application", "domain", "infrastructure"}
|
|
@@ -572,6 +586,10 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
|
|
|
572
586
|
seen_modules.add(module)
|
|
573
587
|
module_names.append(module)
|
|
574
588
|
|
|
589
|
+
_ctrl_note = (
|
|
590
|
+
f"{controller_methods} @RequestMapping methods across "
|
|
591
|
+
f"{controller_classes} controller classes"
|
|
592
|
+
)
|
|
575
593
|
if len(module_names) > 30:
|
|
576
594
|
# Group by first path segment under ddd/ (inferred domain area)
|
|
577
595
|
domain_groups: dict[str, list[str]] = {}
|
|
@@ -590,12 +608,16 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
|
|
|
590
608
|
if module not in domain_groups[domain_prefix or "other"]:
|
|
591
609
|
domain_groups[domain_prefix or "other"].append(module)
|
|
592
610
|
result["controllers"] = {
|
|
593
|
-
"
|
|
611
|
+
"classes": controller_classes,
|
|
612
|
+
"methods": controller_methods,
|
|
613
|
+
"note": _ctrl_note,
|
|
594
614
|
"modules": {k: sorted(v) for k, v in sorted(domain_groups.items())},
|
|
595
615
|
}
|
|
596
616
|
else:
|
|
597
617
|
result["controllers"] = {
|
|
598
|
-
"
|
|
618
|
+
"classes": controller_classes,
|
|
619
|
+
"methods": controller_methods,
|
|
620
|
+
"note": _ctrl_note,
|
|
599
621
|
"modules": sorted(module_names),
|
|
600
622
|
}
|
|
601
623
|
return result
|
|
@@ -1305,6 +1327,13 @@ def compact_view(sm: SourceMap, *, no_tree: bool = False, full: bool = False) ->
|
|
|
1305
1327
|
result["transactional_boundaries"] = _transactional
|
|
1306
1328
|
if _git_ctx:
|
|
1307
1329
|
result["git_context"] = _git_ctx
|
|
1330
|
+
# Angular structural analysis (GAP-10)
|
|
1331
|
+
if sm.project_type in ("angular-spa", "webapp") or any(
|
|
1332
|
+
any(f.name == "Angular" for f in s.frameworks) for s in sm.stacks
|
|
1333
|
+
):
|
|
1334
|
+
_ang = _angular_analysis(sm)
|
|
1335
|
+
if _ang and (_ang.get("component_count", 0) > 0 or _ang.get("angular_version")):
|
|
1336
|
+
result["angular_analysis"] = _ang
|
|
1308
1337
|
if _spring_profiles:
|
|
1309
1338
|
result["spring_profiles"] = _spring_profiles
|
|
1310
1339
|
_always_include = {"project_type", "project_summary", "architecture_summary", "dependency_summary"}
|
|
@@ -1589,6 +1618,88 @@ def validate_cross_analyzer_consistency(
|
|
|
1589
1618
|
return findings
|
|
1590
1619
|
|
|
1591
1620
|
|
|
1621
|
+
def _angular_analysis(sm: "SourceMap") -> "Optional[dict[str, Any]]":
|
|
1622
|
+
"""Extract Angular structural metrics for TypeScript/Angular projects (GAP-10)."""
|
|
1623
|
+
import json as _json
|
|
1624
|
+
import re as _re
|
|
1625
|
+
|
|
1626
|
+
ts_files = [p for p in sm.file_paths if p.endswith(".ts") and not p.endswith(".d.ts")]
|
|
1627
|
+
if not ts_files:
|
|
1628
|
+
return None
|
|
1629
|
+
|
|
1630
|
+
root = Path(sm.metadata.analyzed_path) if sm.metadata.analyzed_path else Path(".")
|
|
1631
|
+
|
|
1632
|
+
component_count = 0
|
|
1633
|
+
service_count = 0
|
|
1634
|
+
lazy_routes_count = 0
|
|
1635
|
+
akita_stores = 0
|
|
1636
|
+
standalone_components = False
|
|
1637
|
+
route_paths: list[str] = []
|
|
1638
|
+
|
|
1639
|
+
for rel in ts_files:
|
|
1640
|
+
try:
|
|
1641
|
+
content = (root / rel).read_text(encoding="utf-8", errors="replace")
|
|
1642
|
+
except OSError:
|
|
1643
|
+
continue
|
|
1644
|
+
component_count += content.count("@Component(")
|
|
1645
|
+
service_count += content.count("@Injectable(")
|
|
1646
|
+
lazy_routes_count += content.count("loadChildren(")
|
|
1647
|
+
akita_stores += content.count("@StoreConfig(")
|
|
1648
|
+
if not standalone_components and "bootstrapApplication(" in content:
|
|
1649
|
+
standalone_components = True
|
|
1650
|
+
# Route tree: parse path: '...' in routing files
|
|
1651
|
+
fname = rel.replace("\\", "/").split("/")[-1]
|
|
1652
|
+
if "routing" in fname or fname in ("app.routes.ts",):
|
|
1653
|
+
for m in _re.finditer(r"path\s*:\s*['\"]([^'\"]*)['\"]", content):
|
|
1654
|
+
val = m.group(1)
|
|
1655
|
+
if val and val not in route_paths:
|
|
1656
|
+
route_paths.append(val)
|
|
1657
|
+
|
|
1658
|
+
# Angular version from package.json
|
|
1659
|
+
angular_version: Optional[str] = None
|
|
1660
|
+
pkg_json = root / "package.json"
|
|
1661
|
+
if pkg_json.exists():
|
|
1662
|
+
try:
|
|
1663
|
+
pkg = _json.loads(pkg_json.read_text(encoding="utf-8", errors="replace"))
|
|
1664
|
+
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
1665
|
+
av = deps.get("@angular/core")
|
|
1666
|
+
if av:
|
|
1667
|
+
angular_version = av.lstrip("^~>=")
|
|
1668
|
+
except Exception:
|
|
1669
|
+
pass
|
|
1670
|
+
|
|
1671
|
+
# Also check angular.json for entry point
|
|
1672
|
+
entry_point: Optional[str] = None
|
|
1673
|
+
angular_json = root / "angular.json"
|
|
1674
|
+
if angular_json.exists():
|
|
1675
|
+
try:
|
|
1676
|
+
aj = _json.loads(angular_json.read_text(encoding="utf-8", errors="replace"))
|
|
1677
|
+
projects = aj.get("projects") or {}
|
|
1678
|
+
for proj in projects.values():
|
|
1679
|
+
main = (
|
|
1680
|
+
(proj.get("architect") or {})
|
|
1681
|
+
.get("build", {})
|
|
1682
|
+
.get("options", {})
|
|
1683
|
+
.get("main")
|
|
1684
|
+
)
|
|
1685
|
+
if main:
|
|
1686
|
+
entry_point = main
|
|
1687
|
+
break
|
|
1688
|
+
except Exception:
|
|
1689
|
+
pass
|
|
1690
|
+
|
|
1691
|
+
return {
|
|
1692
|
+
"angular_version": angular_version,
|
|
1693
|
+
"standalone_components": standalone_components,
|
|
1694
|
+
"lazy_routes_count": lazy_routes_count,
|
|
1695
|
+
"akita_stores": akita_stores,
|
|
1696
|
+
"route_tree": route_paths[:20],
|
|
1697
|
+
"component_count": component_count,
|
|
1698
|
+
"service_count": service_count,
|
|
1699
|
+
**({"entry_point": entry_point} if entry_point else {}),
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
|
|
1592
1703
|
def agent_view(sm: SourceMap, *, full: bool = False) -> dict[str, Any]:
|
|
1593
1704
|
"""Opinionated output for AI agents — structured, noise-free, gap-aware.
|
|
1594
1705
|
|
|
@@ -1813,6 +1924,14 @@ def agent_view(sm: SourceMap, *, full: bool = False) -> dict[str, Any]:
|
|
|
1813
1924
|
if signals:
|
|
1814
1925
|
result["signals"] = signals
|
|
1815
1926
|
|
|
1927
|
+
# ── 8b. Angular structural analysis (GAP-10) ──────────────────────────────
|
|
1928
|
+
if sm.project_type in ("angular-spa", "webapp") or any(
|
|
1929
|
+
any(f.name == "Angular" for f in s.frameworks) for s in sm.stacks
|
|
1930
|
+
):
|
|
1931
|
+
_ang = _angular_analysis(sm)
|
|
1932
|
+
if _ang and (_ang.get("component_count", 0) > 0 or _ang.get("angular_version")):
|
|
1933
|
+
result["angular_analysis"] = _ang
|
|
1934
|
+
|
|
1816
1935
|
# ── 9. Git context — lightweight (top-5 hotspots, branch, uncommitted count)
|
|
1817
1936
|
_gc = _compact_git_context(sm)
|
|
1818
1937
|
if _gc:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.30.
|
|
3
|
+
Version: 1.30.29
|
|
4
4
|
Summary: Deterministic codebase context for AI coding agents
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -221,7 +221,7 @@ Description-Content-Type: text/markdown
|
|
|
221
221
|
|
|
222
222
|
**Deterministic, behavior-aware codebase context for AI agents and PR review.**
|
|
223
223
|
|
|
224
|
-

|
|
225
225
|

|
|
226
226
|
|
|
227
227
|
---
|
|
@@ -257,7 +257,7 @@ pipx install sourcecode
|
|
|
257
257
|
|
|
258
258
|
```bash
|
|
259
259
|
sourcecode version
|
|
260
|
-
# sourcecode 1.30.
|
|
260
|
+
# sourcecode 1.30.29
|
|
261
261
|
```
|
|
262
262
|
|
|
263
263
|
---
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
2
|
-
sourcecode/adaptive_scanner.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=dlUu_PMRN5Q_pvsTBF3LbFJMORBN4BVQ0B9j6mA3GWQ,104
|
|
2
|
+
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=MyBa0Hf5HmkudZQDLKrjcWDKETXETXl0mQX1swtTwAA,39091
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=XgrZg2DcWcUm9r87cRG3KGO7IK2TIL_N-CvhSbUmmh4,49901
|
|
6
|
-
sourcecode/classifier.py,sha256
|
|
7
|
-
sourcecode/cli.py,sha256=
|
|
6
|
+
sourcecode/classifier.py,sha256=-0t0HLc9L9UleMLfclfLM3AXhBjUb_AYyBPDbvgWtac,7755
|
|
7
|
+
sourcecode/cli.py,sha256=5hKnqahlEko1Z1GV1pg0uzEH2hES3RRqxJ95CRBgXwc,98320
|
|
8
8
|
sourcecode/code_notes_analyzer.py,sha256=y1MJBnPZHYp4i6cQCXUb9ATIyifS_qMQWjw_8lPkpsU,9215
|
|
9
9
|
sourcecode/confidence_analyzer.py,sha256=H9VHYRzZhqMFlSCZffjtsMUGYLnDvrq1g5FjzyQ1hxE,16381
|
|
10
10
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
@@ -22,18 +22,18 @@ sourcecode/git_analyzer.py,sha256=0Gyj-vMpIIN4nfriKXVRouNYBeJ59s6pQDX2Xu9Pq-U,13
|
|
|
22
22
|
sourcecode/graph_analyzer.py,sha256=iUK-7pSV-cvGqqD2hENdYmhnm0wcXFEyK-xnu5ul8OU,62515
|
|
23
23
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
24
24
|
sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
|
|
25
|
-
sourcecode/prepare_context.py,sha256=
|
|
25
|
+
sourcecode/prepare_context.py,sha256=Too1ziyHGGvZfNhCMfgCvrTI01xYQeUrwA4veXtgTSI,172167
|
|
26
26
|
sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
27
27
|
sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
|
|
28
28
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
29
29
|
sourcecode/relevance_scorer.py,sha256=MYF4FFkveAQps9SmTeTlh6ODiBz2F--_hWNeHMLtUHQ,8405
|
|
30
30
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
31
|
-
sourcecode/repository_ir.py,sha256=
|
|
31
|
+
sourcecode/repository_ir.py,sha256=KH5EehbjOh8ZwwTHcbzrAHiKDoquO49wgSvCX4bVq5k,64391
|
|
32
32
|
sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBGPjQ,14792
|
|
33
|
-
sourcecode/scanner.py,sha256=
|
|
33
|
+
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
34
34
|
sourcecode/schema.py,sha256=fj3BZ3IcnNV4j21BFIEvz8Qnw_vZoqIbzzRg-qQ-nd0,24530
|
|
35
35
|
sourcecode/semantic_analyzer.py,sha256=12TwXYkYbDcBdu0heX_EmfPM2EkO8a_r5osf0SaeQbs,88956
|
|
36
|
-
sourcecode/serializer.py,sha256=
|
|
36
|
+
sourcecode/serializer.py,sha256=JaEJonNIKaSV6dST3Jn8_Z6alYK9Ba1-M3luh8Xf-yw,110442
|
|
37
37
|
sourcecode/summarizer.py,sha256=lPlKhMh28nueXkPo2xKeD3DUFYVGRlJMIdY-8TSM-ls,17486
|
|
38
38
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
39
39
|
sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
|
|
@@ -48,7 +48,7 @@ sourcecode/detectors/heuristic.py,sha256=bCqqgbHavl4Sse3dqT8mwmo1wAdgeJr7VyXOmfC
|
|
|
48
48
|
sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
|
|
49
49
|
sourcecode/detectors/java.py,sha256=ldvaDZJADAKslOpS5qJ0bxexgeY6h_4Yx4NCtHio7J8,24203
|
|
50
50
|
sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
|
|
51
|
-
sourcecode/detectors/nodejs.py,sha256=
|
|
51
|
+
sourcecode/detectors/nodejs.py,sha256=Hg3Gmr7yIMJFiLoDwOTk2wtu00wxIs6kZf-oQujTFUA,13187
|
|
52
52
|
sourcecode/detectors/parsers.py,sha256=ugPg8yNUf0Ai1gA7Fnn6wAkYGFjTxRodSP3IeViYJJ4,2290
|
|
53
53
|
sourcecode/detectors/php.py,sha256=W_AQD0WMVDdWHa9h_ilX6W8XSpz0X4ctpMK2WXfXf1I,1887
|
|
54
54
|
sourcecode/detectors/project.py,sha256=ghWWOlqg2_uywzeZX573CCkFWQPlc8bBGvvX1BfIDYI,8315
|
|
@@ -64,8 +64,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
64
64
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
65
65
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
66
66
|
sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
|
|
67
|
-
sourcecode-1.30.
|
|
68
|
-
sourcecode-1.30.
|
|
69
|
-
sourcecode-1.30.
|
|
70
|
-
sourcecode-1.30.
|
|
71
|
-
sourcecode-1.30.
|
|
67
|
+
sourcecode-1.30.29.dist-info/METADATA,sha256=39QsgfOfPa-UUti5nV0iLi-gH7KSIz48owb5RoyX12Q,28956
|
|
68
|
+
sourcecode-1.30.29.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
69
|
+
sourcecode-1.30.29.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
70
|
+
sourcecode-1.30.29.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
71
|
+
sourcecode-1.30.29.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|