sourcecode 0.22.0__py3-none-any.whl → 0.23.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/cli.py +37 -5
- sourcecode/confidence_analyzer.py +190 -0
- sourcecode/dependency_analyzer.py +51 -5
- sourcecode/detectors/heuristic.py +9 -1
- sourcecode/detectors/python.py +6 -0
- sourcecode/prepare_context.py +216 -13
- sourcecode/schema.py +30 -1
- sourcecode/serializer.py +108 -53
- {sourcecode-0.22.0.dist-info → sourcecode-0.23.0.dist-info}/METADATA +1 -1
- {sourcecode-0.22.0.dist-info → sourcecode-0.23.0.dist-info}/RECORD +13 -12
- {sourcecode-0.22.0.dist-info → sourcecode-0.23.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.22.0.dist-info → sourcecode-0.23.0.dist-info}/entry_points.txt +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -623,6 +623,12 @@ def main(
|
|
|
623
623
|
for _finding in validate_cross_analyzer_consistency(sm, strict=False):
|
|
624
624
|
typer.echo(f"[consistency] {_finding}", err=True)
|
|
625
625
|
|
|
626
|
+
# Build confidence summary + analysis gaps (always runs, lightweight)
|
|
627
|
+
from sourcecode.confidence_analyzer import ConfidenceAnalyzer
|
|
628
|
+
from dataclasses import replace as _replace
|
|
629
|
+
_conf_summary, _analysis_gaps = ConfidenceAnalyzer().analyze(sm)
|
|
630
|
+
sm = _replace(sm, confidence_summary=_conf_summary, analysis_gaps=_analysis_gaps)
|
|
631
|
+
|
|
626
632
|
# 4. Serializar
|
|
627
633
|
if agent:
|
|
628
634
|
data = agent_view(sm)
|
|
@@ -666,13 +672,18 @@ def main(
|
|
|
666
672
|
def prepare_context_cmd(
|
|
667
673
|
task: Optional[str] = typer.Argument(
|
|
668
674
|
None,
|
|
669
|
-
help="Task: explain | fix-bug | refactor | generate-tests",
|
|
675
|
+
help="Task: explain | fix-bug | refactor | generate-tests | onboard | review-pr | delta",
|
|
670
676
|
),
|
|
671
677
|
path: Path = typer.Option(
|
|
672
678
|
Path("."),
|
|
673
679
|
"--path", "-p",
|
|
674
680
|
help="Project directory to analyze (default: current directory)",
|
|
675
681
|
),
|
|
682
|
+
since: Optional[str] = typer.Option(
|
|
683
|
+
None,
|
|
684
|
+
"--since",
|
|
685
|
+
help="Git ref for delta task: show files changed since this ref (e.g. HEAD~3, main)",
|
|
686
|
+
),
|
|
676
687
|
llm_prompt: bool = typer.Option(
|
|
677
688
|
False,
|
|
678
689
|
"--llm-prompt",
|
|
@@ -689,13 +700,24 @@ def prepare_context_cmd(
|
|
|
689
700
|
help="Show what would be analyzed without running it",
|
|
690
701
|
),
|
|
691
702
|
) -> None:
|
|
692
|
-
"""
|
|
703
|
+
"""Compile task-aware context for AI coding agents.
|
|
704
|
+
|
|
705
|
+
\b
|
|
706
|
+
Tasks:
|
|
707
|
+
explain Project overview: structure, entry points, dependencies
|
|
708
|
+
fix-bug Risk-ranked files, suspected areas, code annotations
|
|
709
|
+
refactor Structural issues, improvement opportunities
|
|
710
|
+
generate-tests Untested source files, test gap analysis
|
|
711
|
+
onboard Full project context for a new agent or developer
|
|
712
|
+
review-pr PR review context: changed files + architecture
|
|
713
|
+
delta Incremental context: git-changed files only
|
|
693
714
|
|
|
694
715
|
\b
|
|
695
|
-
|
|
716
|
+
Examples:
|
|
696
717
|
sourcecode . prepare-context explain
|
|
697
718
|
sourcecode . prepare-context fix-bug --path /my/project
|
|
698
|
-
sourcecode . prepare-context
|
|
719
|
+
sourcecode . prepare-context delta --since main
|
|
720
|
+
sourcecode . prepare-context onboard --llm-prompt
|
|
699
721
|
sourcecode . prepare-context --task-help
|
|
700
722
|
"""
|
|
701
723
|
from sourcecode.prepare_context import TASKS, TaskContextBuilder
|
|
@@ -734,22 +756,28 @@ def prepare_context_cmd(
|
|
|
734
756
|
typer.echo(f"path: {target}")
|
|
735
757
|
typer.echo(f"analyzers: dependencies={'yes' if spec.enable_dependencies else 'no'}"
|
|
736
758
|
f", code_notes={'yes' if spec.enable_code_notes else 'no'}")
|
|
759
|
+
if since:
|
|
760
|
+
typer.echo(f"since: {since}")
|
|
737
761
|
typer.echo(f"output: {spec.output_hint}")
|
|
738
762
|
raise typer.Exit()
|
|
739
763
|
|
|
740
764
|
from dataclasses import asdict
|
|
741
765
|
|
|
742
766
|
builder = TaskContextBuilder(target)
|
|
743
|
-
output = builder.build(task)
|
|
767
|
+
output = builder.build(task, since=since)
|
|
744
768
|
|
|
745
769
|
out: dict[str, Any] = {
|
|
746
770
|
"task": output.task,
|
|
747
771
|
"goal": output.goal,
|
|
748
772
|
"project_summary": output.project_summary,
|
|
749
773
|
"architecture_summary": output.architecture_summary,
|
|
774
|
+
"confidence": output.confidence,
|
|
750
775
|
"relevant_files": [asdict(f) for f in output.relevant_files],
|
|
776
|
+
"why_these_files": output.why_these_files,
|
|
751
777
|
"key_dependencies": output.key_dependencies,
|
|
752
778
|
}
|
|
779
|
+
if output.gaps:
|
|
780
|
+
out["gaps"] = output.gaps
|
|
753
781
|
if output.suspected_areas:
|
|
754
782
|
out["suspected_areas"] = output.suspected_areas
|
|
755
783
|
if output.improvement_opportunities:
|
|
@@ -758,6 +786,10 @@ def prepare_context_cmd(
|
|
|
758
786
|
out["test_gaps"] = output.test_gaps
|
|
759
787
|
if output.code_notes_summary:
|
|
760
788
|
out["code_notes_summary"] = output.code_notes_summary
|
|
789
|
+
if output.changed_files:
|
|
790
|
+
out["changed_files"] = output.changed_files
|
|
791
|
+
if output.affected_entry_points:
|
|
792
|
+
out["affected_entry_points"] = output.affected_entry_points
|
|
761
793
|
if output.limitations:
|
|
762
794
|
out["limitations"] = output.limitations
|
|
763
795
|
if llm_prompt:
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""confidence_analyzer.py — Builds ConfidenceSummary and AnalysisGap list from SourceMap.
|
|
2
|
+
|
|
3
|
+
Analyzes detection quality post-facto:
|
|
4
|
+
- Classifies signals as hard (manifest/lockfile) vs soft (heuristic/extension)
|
|
5
|
+
- Identifies auxiliary paths that were found but correctly ignored
|
|
6
|
+
- Detects anomalies (conflicting signals, low-confidence detections)
|
|
7
|
+
- Produces structured analysis gaps for agent consumption
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from sourcecode.schema import AnalysisGap, ConfidenceSummary, SourceMap
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
_AUXILIARY_DIR_PREFIXES = (
|
|
21
|
+
".claude/", ".cursor/", ".vscode/", ".github/", ".idea/",
|
|
22
|
+
".devcontainer/", ".husky/",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_FIXTURE_DIR_SEGMENTS = {"fixtures", "fixture", "testdata", "test_data", "__fixtures__"}
|
|
26
|
+
_TEST_DIR_SEGMENTS = {"tests", "test", "spec", "specs", "__tests__"}
|
|
27
|
+
_DOC_DIR_SEGMENTS = {"docs", "doc", "documentation", "wiki"}
|
|
28
|
+
_GENERATED_DIR_SEGMENTS = {"dist", "build", "target", "out", "output", ".next", "__pycache__"}
|
|
29
|
+
|
|
30
|
+
_HARD_SOURCES = {"manifest", "lockfile", "pyproject.toml", "package.json", "go.mod",
|
|
31
|
+
"Cargo.toml", "pom.xml", "build.gradle"}
|
|
32
|
+
_SOFT_SOURCES = {"heuristic", "code_signal", "convention"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ConfidenceAnalyzer:
|
|
36
|
+
"""Analyzes SourceMap quality and produces confidence + gap metadata."""
|
|
37
|
+
|
|
38
|
+
def analyze(self, sm: SourceMap) -> tuple[ConfidenceSummary, list[AnalysisGap]]:
|
|
39
|
+
hard_signals: list[str] = []
|
|
40
|
+
soft_signals: list[str] = []
|
|
41
|
+
ignored_signals: list[str] = []
|
|
42
|
+
anomalies: list[str] = []
|
|
43
|
+
gaps: list[AnalysisGap] = []
|
|
44
|
+
|
|
45
|
+
# ── Stack signals ─────────────────────────────────────────────────────
|
|
46
|
+
for stack in sm.stacks:
|
|
47
|
+
if stack.detection_method == "manifest" and stack.confidence in ("high", "medium"):
|
|
48
|
+
for manifest in stack.manifests:
|
|
49
|
+
sig = f"stack:{stack.stack} via {manifest}"
|
|
50
|
+
if sig not in hard_signals:
|
|
51
|
+
hard_signals.append(sig)
|
|
52
|
+
elif stack.detection_method == "heuristic":
|
|
53
|
+
sig = f"stack:{stack.stack} (heuristic, no manifest)"
|
|
54
|
+
if sig not in soft_signals:
|
|
55
|
+
soft_signals.append(sig)
|
|
56
|
+
elif stack.detection_method == "lockfile":
|
|
57
|
+
sig = f"stack:{stack.stack} via lockfile"
|
|
58
|
+
if sig not in hard_signals:
|
|
59
|
+
hard_signals.append(sig)
|
|
60
|
+
|
|
61
|
+
# ── Entry point signals ───────────────────────────────────────────────
|
|
62
|
+
for ep in sm.entry_points:
|
|
63
|
+
if ep.source in _HARD_SOURCES or ep.reason == "console_script":
|
|
64
|
+
sig = f"entry:{ep.path} ({ep.reason or ep.source})"
|
|
65
|
+
if sig not in hard_signals:
|
|
66
|
+
hard_signals.append(sig)
|
|
67
|
+
else:
|
|
68
|
+
sig = f"entry:{ep.path} ({ep.reason or ep.source})"
|
|
69
|
+
if sig not in soft_signals:
|
|
70
|
+
soft_signals.append(sig)
|
|
71
|
+
|
|
72
|
+
# ── Ignored auxiliary paths ───────────────────────────────────────────
|
|
73
|
+
aux_dirs_found: set[str] = set()
|
|
74
|
+
for path in sm.file_paths:
|
|
75
|
+
norm = path.replace("\\", "/")
|
|
76
|
+
for prefix in _AUXILIARY_DIR_PREFIXES:
|
|
77
|
+
if norm.startswith(prefix):
|
|
78
|
+
top = prefix.rstrip("/")
|
|
79
|
+
aux_dirs_found.add(top)
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
for aux in sorted(aux_dirs_found):
|
|
83
|
+
ignored_signals.append(f"aux_dir:{aux} (tooling, not analyzed as project source)")
|
|
84
|
+
|
|
85
|
+
# ── Anomaly: multiple stacks, ambiguous primary ───────────────────────
|
|
86
|
+
primary_stacks = [s for s in sm.stacks if s.primary]
|
|
87
|
+
heuristic_only = [s for s in sm.stacks if s.detection_method == "heuristic"]
|
|
88
|
+
|
|
89
|
+
if len(primary_stacks) == 0 and sm.stacks:
|
|
90
|
+
anomalies.append("No primary stack marked — multiple stacks detected with equal weight")
|
|
91
|
+
if len(primary_stacks) > 1:
|
|
92
|
+
names = ", ".join(s.stack for s in primary_stacks)
|
|
93
|
+
anomalies.append(f"Multiple stacks marked as primary: {names}")
|
|
94
|
+
if heuristic_only and not any(s.detection_method != "heuristic" for s in sm.stacks):
|
|
95
|
+
anomalies.append("All stacks detected via heuristic only — no manifest found")
|
|
96
|
+
|
|
97
|
+
# ── Anomaly: entry points all low-confidence ──────────────────────────
|
|
98
|
+
if sm.entry_points and all(ep.confidence == "low" for ep in sm.entry_points):
|
|
99
|
+
anomalies.append("All entry points are low-confidence (heuristic/code_signal only)")
|
|
100
|
+
|
|
101
|
+
# ── Gaps ──────────────────────────────────────────────────────────────
|
|
102
|
+
if not sm.entry_points:
|
|
103
|
+
gaps.append(AnalysisGap(
|
|
104
|
+
area="entry_points",
|
|
105
|
+
reason="No entry point detected — project may use non-standard structure or be a library",
|
|
106
|
+
impact="high",
|
|
107
|
+
))
|
|
108
|
+
elif all(ep.confidence == "low" for ep in sm.entry_points):
|
|
109
|
+
gaps.append(AnalysisGap(
|
|
110
|
+
area="entry_points",
|
|
111
|
+
reason="Entry points inferred from code patterns only, no manifest declaration found",
|
|
112
|
+
impact="medium",
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
if not sm.stacks:
|
|
116
|
+
gaps.append(AnalysisGap(
|
|
117
|
+
area="stack",
|
|
118
|
+
reason="No stack detected — project may be infrastructure-only or use an unsupported language",
|
|
119
|
+
impact="high",
|
|
120
|
+
))
|
|
121
|
+
elif all(s.detection_method == "heuristic" for s in sm.stacks):
|
|
122
|
+
gaps.append(AnalysisGap(
|
|
123
|
+
area="stack",
|
|
124
|
+
reason="Stack inferred from file extensions only — no manifest or lockfile found",
|
|
125
|
+
impact="medium",
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
dep_summary = sm.dependency_summary
|
|
129
|
+
if dep_summary is None or not dep_summary.requested:
|
|
130
|
+
gaps.append(AnalysisGap(
|
|
131
|
+
area="dependencies",
|
|
132
|
+
reason="Dependencies not analyzed — run with --dependencies for full context",
|
|
133
|
+
impact="medium",
|
|
134
|
+
))
|
|
135
|
+
elif dep_summary.requested and dep_summary.total_count == 0:
|
|
136
|
+
gaps.append(AnalysisGap(
|
|
137
|
+
area="dependencies",
|
|
138
|
+
reason="No dependencies found — project may have no external dependencies or manifest is non-standard",
|
|
139
|
+
impact="low",
|
|
140
|
+
))
|
|
141
|
+
|
|
142
|
+
env_summary = sm.env_summary
|
|
143
|
+
if env_summary is None or not env_summary.requested:
|
|
144
|
+
gaps.append(AnalysisGap(
|
|
145
|
+
area="env",
|
|
146
|
+
reason="Environment variables not analyzed — run with --env-map for operational context",
|
|
147
|
+
impact="low",
|
|
148
|
+
))
|
|
149
|
+
|
|
150
|
+
# ── Compute overall confidence ─────────────────────────────────────────
|
|
151
|
+
# Stack: use best manifest-detected stack, fall back to min
|
|
152
|
+
manifest_stacks = [s for s in sm.stacks if s.detection_method != "heuristic"]
|
|
153
|
+
stack_conf = (
|
|
154
|
+
_max_confidence([s.confidence for s in manifest_stacks])
|
|
155
|
+
if manifest_stacks
|
|
156
|
+
else _min_confidence([s.confidence for s in sm.stacks] or ["low"])
|
|
157
|
+
)
|
|
158
|
+
# Entry points: use best available (highest-confidence EP wins)
|
|
159
|
+
ep_conf = _max_confidence([ep.confidence for ep in sm.entry_points] or ["low"])
|
|
160
|
+
overall = _min_confidence([stack_conf, ep_conf])
|
|
161
|
+
|
|
162
|
+
# Downgrade if gaps are severe
|
|
163
|
+
high_impact_gaps = [g for g in gaps if g.impact == "high"]
|
|
164
|
+
if high_impact_gaps:
|
|
165
|
+
overall = "low" if overall != "high" else "medium"
|
|
166
|
+
|
|
167
|
+
summary = ConfidenceSummary(
|
|
168
|
+
overall=overall, # type: ignore[arg-type]
|
|
169
|
+
stack_confidence=stack_conf, # type: ignore[arg-type]
|
|
170
|
+
entry_point_confidence=ep_conf, # type: ignore[arg-type]
|
|
171
|
+
hard_signals=hard_signals,
|
|
172
|
+
soft_signals=soft_signals,
|
|
173
|
+
ignored_signals=ignored_signals,
|
|
174
|
+
anomalies=anomalies,
|
|
175
|
+
)
|
|
176
|
+
return summary, gaps
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _min_confidence(values: list[str]) -> str:
|
|
180
|
+
rank = {"high": 2, "medium": 1, "low": 0}
|
|
181
|
+
if not values:
|
|
182
|
+
return "low"
|
|
183
|
+
return min(values, key=lambda v: rank.get(v, 0))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _max_confidence(values: list[str]) -> str:
|
|
187
|
+
rank = {"high": 2, "medium": 1, "low": 0}
|
|
188
|
+
if not values:
|
|
189
|
+
return "low"
|
|
190
|
+
return max(values, key=lambda v: rank.get(v, 0))
|
|
@@ -32,6 +32,22 @@ _PY_PARSING: frozenset[str] = frozenset({
|
|
|
32
32
|
"pathspec", "gitpython", "lxml", "beautifulsoup4", "html5lib",
|
|
33
33
|
"regex", "pyparsing",
|
|
34
34
|
})
|
|
35
|
+
_PY_BUILDTOOLS: frozenset[str] = frozenset({
|
|
36
|
+
"setuptools", "hatchling", "flit-core", "flit", "wheel", "build", "twine",
|
|
37
|
+
"hatch", "poetry-core", "maturin", "cython",
|
|
38
|
+
})
|
|
39
|
+
_PY_OBSERVABILITY: frozenset[str] = frozenset({
|
|
40
|
+
"sentry-sdk", "datadog", "prometheus-client", "opentelemetry-api",
|
|
41
|
+
"opentelemetry-sdk", "structlog", "loguru", "elastic-apm", "ddtrace",
|
|
42
|
+
"opentelemetry-instrumentation",
|
|
43
|
+
})
|
|
44
|
+
_PY_INFRA: frozenset[str] = frozenset({
|
|
45
|
+
"boto3", "botocore", "azure-storage-blob", "azure-identity",
|
|
46
|
+
"google-cloud-storage", "google-cloud-bigquery", "kubernetes",
|
|
47
|
+
"paramiko", "fabric", "celery", "dramatiq", "redis", "aioredis",
|
|
48
|
+
"psycopg2", "psycopg2-binary", "asyncpg", "sqlalchemy", "motor",
|
|
49
|
+
"pymongo", "aiomysql", "mysql-connector-python",
|
|
50
|
+
})
|
|
35
51
|
|
|
36
52
|
_NODE_TESTTOOLS: frozenset[str] = frozenset({
|
|
37
53
|
"jest", "@jest/globals", "mocha", "jasmine", "chai", "karma",
|
|
@@ -39,8 +55,22 @@ _NODE_TESTTOOLS: frozenset[str] = frozenset({
|
|
|
39
55
|
"sinon", "nock", "supertest",
|
|
40
56
|
})
|
|
41
57
|
_NODE_DEVTOOLS: frozenset[str] = frozenset({
|
|
42
|
-
"eslint", "prettier", "typescript", "@types/node", "
|
|
43
|
-
"
|
|
58
|
+
"eslint", "prettier", "typescript", "@types/node", "husky",
|
|
59
|
+
"lint-staged", "nodemon", "ts-node", "tsx",
|
|
60
|
+
})
|
|
61
|
+
_NODE_BUILDTOOLS: frozenset[str] = frozenset({
|
|
62
|
+
"webpack", "webpack-cli", "rollup", "parcel", "@babel/core", "@babel/preset-env",
|
|
63
|
+
"esbuild", "@swc/core", "vite", "turbopack", "postcss", "autoprefixer",
|
|
64
|
+
"tailwindcss", "sass", "less",
|
|
65
|
+
})
|
|
66
|
+
_NODE_OBSERVABILITY: frozenset[str] = frozenset({
|
|
67
|
+
"@sentry/node", "dd-trace", "pino", "winston", "morgan",
|
|
68
|
+
"@opentelemetry/sdk-node", "pino-http",
|
|
69
|
+
})
|
|
70
|
+
_NODE_INFRA: frozenset[str] = frozenset({
|
|
71
|
+
"aws-sdk", "@aws-sdk/client-s3", "azure-storage", "firebase-admin",
|
|
72
|
+
"bull", "bullmq", "amqplib", "kafkajs", "ioredis", "pg", "mysql2",
|
|
73
|
+
"mongoose", "prisma", "@prisma/client", "typeorm", "sequelize",
|
|
44
74
|
})
|
|
45
75
|
|
|
46
76
|
_DEV_SCOPES: frozenset[str] = frozenset({"dev", "optional"})
|
|
@@ -49,13 +79,17 @@ _ROLE_PRIORITY: dict[str, int] = {
|
|
|
49
79
|
"runtime": 0,
|
|
50
80
|
"parsing": 1,
|
|
51
81
|
"serialization": 2,
|
|
52
|
-
"
|
|
53
|
-
"
|
|
82
|
+
"observability": 3,
|
|
83
|
+
"infra": 4,
|
|
84
|
+
"buildtool": 5,
|
|
85
|
+
"testtool": 6,
|
|
86
|
+
"devtool": 7,
|
|
87
|
+
"unknown": 8,
|
|
54
88
|
}
|
|
55
89
|
|
|
56
90
|
|
|
57
91
|
def _infer_role(name: str, ecosystem: str, scope: str) -> str:
|
|
58
|
-
"""Infer dependency role: runtime | parsing | serialization | devtool | testtool."""
|
|
92
|
+
"""Infer dependency role: runtime | parsing | serialization | buildtool | observability | infra | devtool | testtool | unknown."""
|
|
59
93
|
n = name.lower()
|
|
60
94
|
is_dev = scope in _DEV_SCOPES or scope.startswith(("optional:", "group:"))
|
|
61
95
|
|
|
@@ -64,8 +98,14 @@ def _infer_role(name: str, ecosystem: str, scope: str) -> str:
|
|
|
64
98
|
return "testtool"
|
|
65
99
|
if n in _PY_DEVTOOLS:
|
|
66
100
|
return "devtool"
|
|
101
|
+
if n in _PY_BUILDTOOLS:
|
|
102
|
+
return "buildtool"
|
|
67
103
|
if is_dev:
|
|
68
104
|
return "devtool"
|
|
105
|
+
if n in _PY_OBSERVABILITY:
|
|
106
|
+
return "observability"
|
|
107
|
+
if n in _PY_INFRA:
|
|
108
|
+
return "infra"
|
|
69
109
|
if n in _PY_SERIALIZATION:
|
|
70
110
|
return "serialization"
|
|
71
111
|
if n in _PY_PARSING:
|
|
@@ -77,8 +117,14 @@ def _infer_role(name: str, ecosystem: str, scope: str) -> str:
|
|
|
77
117
|
return "testtool"
|
|
78
118
|
if n in _NODE_DEVTOOLS:
|
|
79
119
|
return "devtool"
|
|
120
|
+
if n in _NODE_BUILDTOOLS:
|
|
121
|
+
return "buildtool"
|
|
80
122
|
if is_dev:
|
|
81
123
|
return "devtool"
|
|
124
|
+
if n in _NODE_OBSERVABILITY:
|
|
125
|
+
return "observability"
|
|
126
|
+
if n in _NODE_INFRA:
|
|
127
|
+
return "infra"
|
|
82
128
|
return "runtime"
|
|
83
129
|
|
|
84
130
|
return "devtool" if is_dev else "runtime"
|
|
@@ -66,6 +66,14 @@ class HeuristicDetector(AbstractDetector):
|
|
|
66
66
|
if filename in _ENTRYPOINT_NAMES:
|
|
67
67
|
stack, kind = _ENTRYPOINT_NAMES[filename]
|
|
68
68
|
entry_points.append(
|
|
69
|
-
EntryPoint(
|
|
69
|
+
EntryPoint(
|
|
70
|
+
path=path,
|
|
71
|
+
stack=stack,
|
|
72
|
+
kind=kind,
|
|
73
|
+
source="heuristic",
|
|
74
|
+
confidence="low",
|
|
75
|
+
reason="entry_file_pattern",
|
|
76
|
+
evidence=f"conventional filename: {filename}",
|
|
77
|
+
)
|
|
70
78
|
)
|
|
71
79
|
return stacks, entry_points
|
sourcecode/detectors/python.py
CHANGED
|
@@ -162,6 +162,8 @@ class PythonDetector(AbstractDetector):
|
|
|
162
162
|
kind=kind,
|
|
163
163
|
source="pyproject.toml",
|
|
164
164
|
confidence="high",
|
|
165
|
+
reason="console_script",
|
|
166
|
+
evidence="declared in [project.scripts]",
|
|
165
167
|
)
|
|
166
168
|
)
|
|
167
169
|
|
|
@@ -180,6 +182,8 @@ class PythonDetector(AbstractDetector):
|
|
|
180
182
|
kind=kind,
|
|
181
183
|
source="convention",
|
|
182
184
|
confidence="medium",
|
|
185
|
+
reason="entry_file_pattern",
|
|
186
|
+
evidence=f"conventional filename: {fname}",
|
|
183
187
|
)
|
|
184
188
|
)
|
|
185
189
|
|
|
@@ -195,6 +199,8 @@ class PythonDetector(AbstractDetector):
|
|
|
195
199
|
kind="script",
|
|
196
200
|
source="code_signal",
|
|
197
201
|
confidence="low",
|
|
202
|
+
reason="main_guard",
|
|
203
|
+
evidence="if __name__ == '__main__' guard detected",
|
|
198
204
|
)
|
|
199
205
|
)
|
|
200
206
|
|
sourcecode/prepare_context.py
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
"""prepare_context.py — Task-aware context
|
|
1
|
+
"""prepare_context.py — Task-aware context compiler for AI coding agents.
|
|
2
|
+
|
|
3
|
+
Each task produces a focused context bundle:
|
|
4
|
+
- goal: what the agent should accomplish
|
|
5
|
+
- project_summary: value-oriented product description
|
|
6
|
+
- architecture_summary: flow description (entry → processing → output)
|
|
7
|
+
- relevant_files: ranked by relevance with why_these_files rationale
|
|
8
|
+
- key_dependencies: runtime-first, role-tagged
|
|
9
|
+
- confidence: detection quality indicator
|
|
10
|
+
- gaps: what's uncertain or not analyzed
|
|
11
|
+
- llm_prompt: ready-to-use prompt (optional, --llm-prompt)
|
|
12
|
+
"""
|
|
2
13
|
|
|
3
14
|
from __future__ import annotations
|
|
4
15
|
|
|
5
|
-
from dataclasses import dataclass
|
|
16
|
+
from dataclasses import dataclass, field
|
|
6
17
|
from pathlib import Path
|
|
7
18
|
from typing import Any, Optional
|
|
8
19
|
|
|
@@ -105,6 +116,77 @@ untested or undertested areas of this codebase.
|
|
|
105
116
|
4. Each test must have a clear name that describes exactly what it verifies.
|
|
106
117
|
"""
|
|
107
118
|
|
|
119
|
+
_ONBOARD_PROMPT = """\
|
|
120
|
+
You are an expert software engineer onboarding to a new codebase. \
|
|
121
|
+
Your task is to understand and explain this project thoroughly.
|
|
122
|
+
|
|
123
|
+
## Project
|
|
124
|
+
|
|
125
|
+
{project_summary}
|
|
126
|
+
|
|
127
|
+
{architecture_section}
|
|
128
|
+
|
|
129
|
+
## Entry Points
|
|
130
|
+
|
|
131
|
+
{relevant_files_section}
|
|
132
|
+
|
|
133
|
+
{dependencies_section}
|
|
134
|
+
|
|
135
|
+
## Instructions
|
|
136
|
+
|
|
137
|
+
1. Describe what this project does and the problem it solves.
|
|
138
|
+
2. Walk through the main execution flow from entry point to output.
|
|
139
|
+
3. Identify the 3-5 most important files to understand first.
|
|
140
|
+
4. Note any non-obvious conventions, constraints, or design decisions.
|
|
141
|
+
5. List what you would need to know to safely modify this codebase.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
_REVIEW_PR_PROMPT = """\
|
|
145
|
+
You are an expert code reviewer. Your task is to review the changes \
|
|
146
|
+
in this pull request within the context of the full project.
|
|
147
|
+
|
|
148
|
+
## Project Context
|
|
149
|
+
|
|
150
|
+
{project_summary}
|
|
151
|
+
|
|
152
|
+
{architecture_section}
|
|
153
|
+
|
|
154
|
+
## Changed Files
|
|
155
|
+
|
|
156
|
+
{relevant_files_section}
|
|
157
|
+
|
|
158
|
+
{suspected_areas_section}
|
|
159
|
+
|
|
160
|
+
## Instructions
|
|
161
|
+
|
|
162
|
+
1. Review the changed files for correctness, security, and maintainability.
|
|
163
|
+
2. Check that changes are consistent with the project's architecture.
|
|
164
|
+
3. Identify any missing tests, edge cases, or error handling.
|
|
165
|
+
4. Flag any breaking changes to public APIs or contracts.
|
|
166
|
+
5. Suggest concrete improvements with specific file and line references.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
_DELTA_PROMPT = """\
|
|
170
|
+
You are an expert software engineer reviewing incremental changes to a codebase.
|
|
171
|
+
|
|
172
|
+
## Project Context
|
|
173
|
+
|
|
174
|
+
{project_summary}
|
|
175
|
+
|
|
176
|
+
## Changed Files
|
|
177
|
+
|
|
178
|
+
{relevant_files_section}
|
|
179
|
+
|
|
180
|
+
{suspected_areas_section}
|
|
181
|
+
|
|
182
|
+
## Instructions
|
|
183
|
+
|
|
184
|
+
1. Analyze the changed files and their relationship to the project architecture.
|
|
185
|
+
2. Identify which entry points and components are affected.
|
|
186
|
+
3. Assess the risk and impact of these changes.
|
|
187
|
+
4. Flag any consistency issues with the rest of the codebase.
|
|
188
|
+
"""
|
|
189
|
+
|
|
108
190
|
|
|
109
191
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
110
192
|
# Task registry
|
|
@@ -170,6 +252,41 @@ TASKS: dict[str, TaskSpec] = {
|
|
|
170
252
|
prompt_template=_GENERATE_TESTS_PROMPT,
|
|
171
253
|
output_hint="test_gaps, relevant_files (source without tests), key_dependencies",
|
|
172
254
|
),
|
|
255
|
+
"onboard": TaskSpec(
|
|
256
|
+
name="onboard",
|
|
257
|
+
goal="Build complete project understanding for a new agent or developer joining the codebase.",
|
|
258
|
+
description="Full structural context: entry points, architecture, key files, dependencies.",
|
|
259
|
+
ranking_boosts=["main", "cli", "app", "core", "index", "schema", "config", "readme"],
|
|
260
|
+
ranking_penalties=[".min.", "__pycache__"],
|
|
261
|
+
enable_code_notes=False,
|
|
262
|
+
enable_dependencies=True,
|
|
263
|
+
prompt_template=_ONBOARD_PROMPT,
|
|
264
|
+
output_hint="project_summary, architecture_summary, relevant_files, key_dependencies, confidence, gaps",
|
|
265
|
+
),
|
|
266
|
+
"review-pr": TaskSpec(
|
|
267
|
+
name="review-pr",
|
|
268
|
+
goal="Review pull request changes in the context of the full project architecture.",
|
|
269
|
+
description="Surface changed files, potential regressions, and architectural consistency.",
|
|
270
|
+
ranking_boosts=["handler", "service", "middleware", "router", "controller",
|
|
271
|
+
"api", "schema", "model", "validator"],
|
|
272
|
+
ranking_penalties=[".min.", "__pycache__"],
|
|
273
|
+
enable_code_notes=True,
|
|
274
|
+
enable_dependencies=False,
|
|
275
|
+
prompt_template=_REVIEW_PR_PROMPT,
|
|
276
|
+
output_hint="relevant_files, suspected_areas, architecture_summary, code_notes_summary",
|
|
277
|
+
),
|
|
278
|
+
"delta": TaskSpec(
|
|
279
|
+
name="delta",
|
|
280
|
+
goal="Produce incremental context for changed files — avoids re-reading the full repo.",
|
|
281
|
+
description="Git-aware context: changed files, affected entry points, dependency impact.",
|
|
282
|
+
ranking_boosts=["handler", "service", "middleware", "router", "controller",
|
|
283
|
+
"api", "schema", "model"],
|
|
284
|
+
ranking_penalties=[".min.", "__pycache__"],
|
|
285
|
+
enable_code_notes=True,
|
|
286
|
+
enable_dependencies=False,
|
|
287
|
+
prompt_template=_DELTA_PROMPT,
|
|
288
|
+
output_hint="changed_files, affected_entry_points, dependency_impact, architecture_summary",
|
|
289
|
+
),
|
|
173
290
|
}
|
|
174
291
|
|
|
175
292
|
|
|
@@ -183,6 +300,7 @@ class RelevantFile:
|
|
|
183
300
|
role: str # entrypoint | source | test
|
|
184
301
|
score: float
|
|
185
302
|
reason: str
|
|
303
|
+
why: str = "" # why this file matters for the specific task
|
|
186
304
|
|
|
187
305
|
|
|
188
306
|
@dataclass
|
|
@@ -198,6 +316,11 @@ class TaskOutput:
|
|
|
198
316
|
key_dependencies: list[dict[str, Any]]
|
|
199
317
|
code_notes_summary: Optional[dict[str, Any]]
|
|
200
318
|
limitations: list[str]
|
|
319
|
+
confidence: str = "medium" # overall detection confidence
|
|
320
|
+
gaps: list[str] = field(default_factory=list) # analysis gaps
|
|
321
|
+
why_these_files: dict[str, str] = field(default_factory=dict) # path → why relevant
|
|
322
|
+
changed_files: list[str] = field(default_factory=list) # delta task only
|
|
323
|
+
affected_entry_points: list[str] = field(default_factory=list) # delta task only
|
|
201
324
|
|
|
202
325
|
|
|
203
326
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -218,7 +341,7 @@ class TaskContextBuilder:
|
|
|
218
341
|
def __init__(self, root: Path) -> None:
|
|
219
342
|
self.root = root
|
|
220
343
|
|
|
221
|
-
def build(self, task_name: str) -> TaskOutput:
|
|
344
|
+
def build(self, task_name: str, *, since: Optional[str] = None) -> TaskOutput:
|
|
222
345
|
if task_name not in TASKS:
|
|
223
346
|
raise ValueError(
|
|
224
347
|
f"Unknown task '{task_name}'. Available: {', '.join(TASKS)}"
|
|
@@ -334,6 +457,42 @@ class TaskContextBuilder:
|
|
|
334
457
|
untested.sort(key=lambda p: (len(p.split("/")), p))
|
|
335
458
|
test_gaps = untested[:15]
|
|
336
459
|
|
|
460
|
+
# ── 8. Confidence + gaps ──────────────────────────────────────────────
|
|
461
|
+
from sourcecode.confidence_analyzer import ConfidenceAnalyzer
|
|
462
|
+
from dataclasses import asdict as _asdict
|
|
463
|
+
|
|
464
|
+
sm_for_conf = SourceMap(
|
|
465
|
+
metadata=AnalysisMetadata(analyzed_path=str(self.root)),
|
|
466
|
+
file_tree=file_tree,
|
|
467
|
+
stacks=stacks,
|
|
468
|
+
project_type=project_type,
|
|
469
|
+
entry_points=entry_points,
|
|
470
|
+
)
|
|
471
|
+
sm_for_conf.file_paths = all_paths
|
|
472
|
+
if spec.enable_dependencies and key_dependencies:
|
|
473
|
+
from sourcecode.schema import DependencySummary
|
|
474
|
+
sm_for_conf.dependency_summary = DependencySummary(
|
|
475
|
+
requested=True,
|
|
476
|
+
total_count=len(key_dependencies),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
conf_summary, analysis_gaps = ConfidenceAnalyzer().analyze(sm_for_conf)
|
|
480
|
+
confidence = conf_summary.overall
|
|
481
|
+
gaps = [g.reason for g in analysis_gaps]
|
|
482
|
+
|
|
483
|
+
# ── 9. why_these_files ────────────────────────────────────────────────
|
|
484
|
+
why_these_files: dict[str, str] = {
|
|
485
|
+
rf.path: rf.reason for rf in relevant_files
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# ── 10. Delta: git changed files ──────────────────────────────────────
|
|
489
|
+
changed_files: list[str] = []
|
|
490
|
+
affected_entry_points: list[str] = []
|
|
491
|
+
if task_name == "delta":
|
|
492
|
+
changed_files = self._get_git_changed_files(since=since)
|
|
493
|
+
ep_set = {ep.path for ep in entry_points}
|
|
494
|
+
affected_entry_points = [f for f in changed_files if f in ep_set]
|
|
495
|
+
|
|
337
496
|
return TaskOutput(
|
|
338
497
|
task=task_name,
|
|
339
498
|
goal=spec.goal,
|
|
@@ -346,6 +505,11 @@ class TaskContextBuilder:
|
|
|
346
505
|
key_dependencies=key_dependencies,
|
|
347
506
|
code_notes_summary=code_notes_summary,
|
|
348
507
|
limitations=limitations,
|
|
508
|
+
confidence=confidence,
|
|
509
|
+
gaps=gaps,
|
|
510
|
+
why_these_files=why_these_files,
|
|
511
|
+
changed_files=changed_files,
|
|
512
|
+
affected_entry_points=affected_entry_points,
|
|
349
513
|
)
|
|
350
514
|
|
|
351
515
|
def render_prompt(self, output: TaskOutput) -> str:
|
|
@@ -398,16 +562,21 @@ class TaskContextBuilder:
|
|
|
398
562
|
"\n".join(f"- `{p}`" for p in output.test_gaps),
|
|
399
563
|
)
|
|
400
564
|
|
|
401
|
-
|
|
402
|
-
project_summary
|
|
403
|
-
architecture_section
|
|
404
|
-
relevant_files_section
|
|
405
|
-
dependencies_section
|
|
406
|
-
suspected_areas_section
|
|
407
|
-
code_notes_section
|
|
408
|
-
improvement_opportunities_section
|
|
409
|
-
test_gaps_section
|
|
410
|
-
|
|
565
|
+
format_kwargs: dict[str, str] = {
|
|
566
|
+
"project_summary": project_summary,
|
|
567
|
+
"architecture_section": architecture_section,
|
|
568
|
+
"relevant_files_section": relevant_files_section,
|
|
569
|
+
"dependencies_section": dependencies_section,
|
|
570
|
+
"suspected_areas_section": suspected_areas_section,
|
|
571
|
+
"code_notes_section": code_notes_section,
|
|
572
|
+
"improvement_opportunities_section": improvement_opportunities_section,
|
|
573
|
+
"test_gaps_section": test_gaps_section,
|
|
574
|
+
}
|
|
575
|
+
# Only pass keys that the template actually uses
|
|
576
|
+
import re as _re
|
|
577
|
+
used_keys = set(_re.findall(r"\{(\w+)\}", spec.prompt_template))
|
|
578
|
+
filtered_kwargs = {k: v for k, v in format_kwargs.items() if k in used_keys}
|
|
579
|
+
return spec.prompt_template.format(**filtered_kwargs).strip()
|
|
411
580
|
|
|
412
581
|
# ── Helpers ───────────────────────────────────────────────────────────────
|
|
413
582
|
|
|
@@ -484,3 +653,37 @@ class TaskContextBuilder:
|
|
|
484
653
|
|
|
485
654
|
def _is_source(self, path: str) -> bool:
|
|
486
655
|
return Path(path).suffix.lower() in _SOURCE_EXTENSIONS
|
|
656
|
+
|
|
657
|
+
def _get_git_changed_files(self, since: Optional[str] = None) -> list[str]:
|
|
658
|
+
"""Get files changed since a git ref (default: HEAD~1) relative to root."""
|
|
659
|
+
import subprocess
|
|
660
|
+
ref = since or "HEAD~1"
|
|
661
|
+
try:
|
|
662
|
+
result = subprocess.run(
|
|
663
|
+
["git", "diff", "--name-only", ref, "HEAD"],
|
|
664
|
+
cwd=str(self.root),
|
|
665
|
+
capture_output=True,
|
|
666
|
+
text=True,
|
|
667
|
+
timeout=10,
|
|
668
|
+
)
|
|
669
|
+
if result.returncode == 0:
|
|
670
|
+
return [
|
|
671
|
+
line.strip() for line in result.stdout.splitlines()
|
|
672
|
+
if line.strip()
|
|
673
|
+
]
|
|
674
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
675
|
+
pass
|
|
676
|
+
# Fallback: uncommitted changes
|
|
677
|
+
try:
|
|
678
|
+
result = subprocess.run(
|
|
679
|
+
["git", "diff", "--name-only"],
|
|
680
|
+
cwd=str(self.root),
|
|
681
|
+
capture_output=True,
|
|
682
|
+
text=True,
|
|
683
|
+
timeout=10,
|
|
684
|
+
)
|
|
685
|
+
if result.returncode == 0:
|
|
686
|
+
return [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
687
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
688
|
+
pass
|
|
689
|
+
return []
|
sourcecode/schema.py
CHANGED
|
@@ -68,6 +68,8 @@ class EntryPoint:
|
|
|
68
68
|
kind: str = "entry"
|
|
69
69
|
source: str = "manifest"
|
|
70
70
|
confidence: Literal["high", "medium", "low"] = "high"
|
|
71
|
+
reason: Optional[str] = None # console_script | entry_file_pattern | main_guard | typer_app | heuristic | convention
|
|
72
|
+
evidence: Optional[str] = None # brief evidence string
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
@dataclass
|
|
@@ -83,7 +85,7 @@ class DependencyRecord:
|
|
|
83
85
|
parent: Optional[str] = None
|
|
84
86
|
manifest_path: Optional[str] = None
|
|
85
87
|
workspace: Optional[str] = None
|
|
86
|
-
role: Optional[str] = None # runtime | parsing | serialization | devtool | testtool
|
|
88
|
+
role: Optional[str] = None # runtime | parsing | serialization | buildtool | observability | infra | devtool | testtool | unknown
|
|
87
89
|
|
|
88
90
|
|
|
89
91
|
@dataclass
|
|
@@ -409,6 +411,30 @@ class CodeNotesSummary:
|
|
|
409
411
|
limitations: list[str] = field(default_factory=list)
|
|
410
412
|
|
|
411
413
|
|
|
414
|
+
# --- Confidence & Explainability ---
|
|
415
|
+
|
|
416
|
+
@dataclass
|
|
417
|
+
class ConfidenceSummary:
|
|
418
|
+
"""Resumen de confianza y calidad del analisis."""
|
|
419
|
+
|
|
420
|
+
overall: Literal["high", "medium", "low"] = "medium"
|
|
421
|
+
stack_confidence: Literal["high", "medium", "low"] = "medium"
|
|
422
|
+
entry_point_confidence: Literal["high", "medium", "low"] = "medium"
|
|
423
|
+
hard_signals: list[str] = field(default_factory=list) # manifest, lockfile, real entrypoint
|
|
424
|
+
soft_signals: list[str] = field(default_factory=list) # heuristic, extension-based
|
|
425
|
+
ignored_signals: list[str] = field(default_factory=list) # tooling dirs, aux manifests
|
|
426
|
+
anomalies: list[str] = field(default_factory=list)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@dataclass
|
|
430
|
+
class AnalysisGap:
|
|
431
|
+
"""Gap o incertidumbre detectada en el analisis."""
|
|
432
|
+
|
|
433
|
+
area: str # entry_points | dependencies | stack | architecture | env
|
|
434
|
+
reason: str
|
|
435
|
+
impact: Literal["high", "medium", "low"] = "medium"
|
|
436
|
+
|
|
437
|
+
|
|
412
438
|
# --- Git Context ---
|
|
413
439
|
|
|
414
440
|
@dataclass
|
|
@@ -501,3 +527,6 @@ class SourceMap:
|
|
|
501
527
|
code_notes: list[CodeNote] = field(default_factory=list)
|
|
502
528
|
code_adrs: list[AdrRecord] = field(default_factory=list)
|
|
503
529
|
code_notes_summary: Optional[CodeNotesSummary] = None
|
|
530
|
+
# Confidence & Explainability (v0.23.0)
|
|
531
|
+
confidence_summary: Optional[ConfidenceSummary] = None
|
|
532
|
+
analysis_gaps: list[AnalysisGap] = field(default_factory=list)
|
sourcecode/serializer.py
CHANGED
|
@@ -55,30 +55,27 @@ def to_yaml(sm: SourceMap) -> str:
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def compact_view(sm: SourceMap, *, no_tree: bool = False) -> dict[str, Any]:
|
|
58
|
-
"""
|
|
58
|
+
"""Context package ready for prompt or handoff (~600-800 tokens).
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
dependency_summary + key_dependencies (cuando requested=True),
|
|
63
|
-
file_tree_depth1 (omitido si no_tree=True).
|
|
60
|
+
Answers: what it is, where it enters, what depends on what,
|
|
61
|
+
what signals matter, and what uncertainty exists.
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
depth1 = {}
|
|
70
|
-
for name, value in sm.file_tree.items():
|
|
71
|
-
if isinstance(value, dict):
|
|
72
|
-
depth1[name] = {}
|
|
73
|
-
else:
|
|
74
|
-
depth1[name] = None
|
|
63
|
+
Includes: project_type, project_summary, architecture_summary,
|
|
64
|
+
stacks, entry_points, dependency_summary + key_dependencies (when analyzed),
|
|
65
|
+
env_summary (when analyzed), code_notes_summary (when analyzed),
|
|
66
|
+
confidence_summary, anomalies, analysis_gaps.
|
|
75
67
|
|
|
68
|
+
Excludes: file_tree, raw dependency lists, docs, module_graph.
|
|
69
|
+
Empty sections are explained when relevant.
|
|
70
|
+
"""
|
|
76
71
|
dep_summary_dict: Any = None
|
|
77
72
|
key_deps: Any = None
|
|
78
73
|
if sm.dependency_summary is not None and sm.dependency_summary.requested:
|
|
79
74
|
dep_summary_dict = asdict(sm.dependency_summary)
|
|
80
75
|
dep_summary_dict.pop("dependencies", None)
|
|
81
76
|
key_deps = [asdict(d) for d in sm.key_dependencies]
|
|
77
|
+
elif sm.dependency_summary is None or not sm.dependency_summary.requested:
|
|
78
|
+
dep_summary_dict = None # "not analyzed" — agent should add --dependencies
|
|
82
79
|
|
|
83
80
|
env_summary_dict: Any = None
|
|
84
81
|
if sm.env_summary is not None and sm.env_summary.requested:
|
|
@@ -88,21 +85,47 @@ def compact_view(sm: SourceMap, *, no_tree: bool = False) -> dict[str, Any]:
|
|
|
88
85
|
if sm.code_notes_summary is not None and sm.code_notes_summary.requested:
|
|
89
86
|
code_notes_summary_dict = asdict(sm.code_notes_summary)
|
|
90
87
|
|
|
88
|
+
# Entry points: strip None fields for compactness
|
|
89
|
+
entry_points_compact = [
|
|
90
|
+
{k: v for k, v in asdict(ep).items() if v is not None and v != ""}
|
|
91
|
+
for ep in sm.entry_points
|
|
92
|
+
]
|
|
93
|
+
if not entry_points_compact:
|
|
94
|
+
entry_points_compact = None # type: ignore[assignment] # signal: not detected
|
|
95
|
+
|
|
96
|
+
# Confidence summary
|
|
97
|
+
conf_dict: Any = None
|
|
98
|
+
anomalies: Any = None
|
|
99
|
+
if sm.confidence_summary is not None:
|
|
100
|
+
conf_dict = asdict(sm.confidence_summary)
|
|
101
|
+
if sm.confidence_summary.anomalies:
|
|
102
|
+
anomalies = sm.confidence_summary.anomalies
|
|
103
|
+
|
|
104
|
+
# Analysis gaps
|
|
105
|
+
gaps_list: Any = None
|
|
106
|
+
if sm.analysis_gaps:
|
|
107
|
+
gaps_list = [asdict(g) for g in sm.analysis_gaps]
|
|
108
|
+
|
|
91
109
|
result: dict[str, Any] = {
|
|
92
110
|
"schema_version": sm.metadata.schema_version,
|
|
93
111
|
"project_type": sm.project_type,
|
|
94
112
|
"project_summary": sm.project_summary,
|
|
95
113
|
"architecture_summary": sm.architecture_summary,
|
|
96
114
|
"stacks": [asdict(stack) for stack in sm.stacks],
|
|
97
|
-
"entry_points":
|
|
115
|
+
"entry_points": entry_points_compact,
|
|
98
116
|
"dependency_summary": dep_summary_dict,
|
|
99
117
|
"key_dependencies": key_deps,
|
|
100
118
|
"env_summary": env_summary_dict,
|
|
101
119
|
"code_notes_summary": code_notes_summary_dict,
|
|
120
|
+
"confidence_summary": conf_dict,
|
|
121
|
+
"anomalies": anomalies,
|
|
122
|
+
"analysis_gaps": gaps_list,
|
|
102
123
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
# Strip keys that are fully None and not informative
|
|
125
|
+
return {k: v for k, v in result.items() if v is not None or k in (
|
|
126
|
+
"project_type", "project_summary", "architecture_summary",
|
|
127
|
+
"dependency_summary", "confidence_summary",
|
|
128
|
+
)}
|
|
106
129
|
|
|
107
130
|
|
|
108
131
|
def normalize_source_map(sm: SourceMap) -> SourceMap:
|
|
@@ -349,15 +372,16 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
|
|
|
349
372
|
"""Opinionated output for AI agents — structured, noise-free, gap-aware.
|
|
350
373
|
|
|
351
374
|
Output order:
|
|
352
|
-
project
|
|
353
|
-
entry_points
|
|
354
|
-
architecture
|
|
355
|
-
key_dependencies →
|
|
356
|
-
signals
|
|
357
|
-
|
|
375
|
+
1. project → identity: type, summary, primary stack, frameworks
|
|
376
|
+
2. entry_points → where execution starts (with reason/evidence)
|
|
377
|
+
3. architecture → how it's structured (flow description)
|
|
378
|
+
4. key_dependencies → runtime dependencies that matter (when analyzed)
|
|
379
|
+
5. signals → compact operational context (env, notes, tests)
|
|
380
|
+
6. confidence_summary → detection quality and hard/soft signals
|
|
381
|
+
7. analysis_gaps → what's uncertain or missing
|
|
358
382
|
|
|
359
383
|
Never includes: file_tree, file_paths, schema internals, empty sections,
|
|
360
|
-
null fields, raw dependency lists, or low-signal metadata.
|
|
384
|
+
null fields, raw dependency lists, metrics, docs, or low-signal metadata.
|
|
361
385
|
"""
|
|
362
386
|
# ── 1. Identity ──────────────────────────────────────────────────────────
|
|
363
387
|
primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
|
|
@@ -381,10 +405,11 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
|
|
|
381
405
|
|
|
382
406
|
result: dict[str, Any] = {"project": project}
|
|
383
407
|
|
|
384
|
-
# ── 2. Entry points
|
|
408
|
+
# ── 2. Entry points (with reason/evidence when available) ─────────────────
|
|
385
409
|
if sm.entry_points:
|
|
410
|
+
_ep_skip = {"workspace"}
|
|
386
411
|
result["entry_points"] = [
|
|
387
|
-
{k: v for k, v in asdict(ep).items() if v is not None and v != ""}
|
|
412
|
+
{k: v for k, v in asdict(ep).items() if v is not None and v != "" and k not in _ep_skip}
|
|
388
413
|
for ep in sm.entry_points
|
|
389
414
|
]
|
|
390
415
|
|
|
@@ -394,9 +419,9 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
|
|
|
394
419
|
|
|
395
420
|
# ── 4. Key dependencies (role-sorted, already computed) ───────────────────
|
|
396
421
|
if sm.dependency_summary and sm.dependency_summary.requested and sm.key_dependencies:
|
|
397
|
-
|
|
422
|
+
_dep_skip = {"parent", "manifest_path", "workspace", "source", "ecosystem"}
|
|
398
423
|
result["key_dependencies"] = [
|
|
399
|
-
{k: v for k, v in asdict(d).items() if v is not None and k not in
|
|
424
|
+
{k: v for k, v in asdict(d).items() if v is not None and k not in _dep_skip}
|
|
400
425
|
for d in sm.key_dependencies
|
|
401
426
|
]
|
|
402
427
|
|
|
@@ -428,29 +453,59 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
|
|
|
428
453
|
if signals:
|
|
429
454
|
result["signals"] = signals
|
|
430
455
|
|
|
431
|
-
# ── 6.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if
|
|
453
|
-
|
|
456
|
+
# ── 6. Confidence summary ─────────────────────────────────────────────────
|
|
457
|
+
if sm.confidence_summary is not None:
|
|
458
|
+
cs = sm.confidence_summary
|
|
459
|
+
conf: dict[str, Any] = {
|
|
460
|
+
"overall": cs.overall,
|
|
461
|
+
"stack": cs.stack_confidence,
|
|
462
|
+
"entry_points": cs.entry_point_confidence,
|
|
463
|
+
}
|
|
464
|
+
if cs.hard_signals:
|
|
465
|
+
conf["hard_signals"] = cs.hard_signals
|
|
466
|
+
if cs.soft_signals:
|
|
467
|
+
conf["soft_signals"] = cs.soft_signals
|
|
468
|
+
if cs.ignored_signals:
|
|
469
|
+
conf["ignored_signals"] = cs.ignored_signals
|
|
470
|
+
if cs.anomalies:
|
|
471
|
+
conf["anomalies"] = cs.anomalies
|
|
472
|
+
result["confidence_summary"] = conf
|
|
473
|
+
|
|
474
|
+
# ── 7. Analysis gaps ──────────────────────────────────────────────────────
|
|
475
|
+
analysis_gaps: list[dict[str, Any]] = []
|
|
476
|
+
|
|
477
|
+
if sm.analysis_gaps:
|
|
478
|
+
analysis_gaps = [asdict(g) for g in sm.analysis_gaps]
|
|
479
|
+
else:
|
|
480
|
+
# Fallback gap derivation when confidence_analyzer was not run
|
|
481
|
+
if not sm.entry_points:
|
|
482
|
+
analysis_gaps.append({
|
|
483
|
+
"area": "entry_points",
|
|
484
|
+
"reason": "No entry point detected — project structure may be non-standard",
|
|
485
|
+
"impact": "high",
|
|
486
|
+
})
|
|
487
|
+
if primary and primary.confidence == "low":
|
|
488
|
+
analysis_gaps.append({
|
|
489
|
+
"area": "stack",
|
|
490
|
+
"reason": f"Low-confidence detection for '{primary.stack}' — no manifest found",
|
|
491
|
+
"impact": "medium",
|
|
492
|
+
})
|
|
493
|
+
heuristic_stacks = [s for s in sm.stacks if s.detection_method == "heuristic"]
|
|
494
|
+
if heuristic_stacks:
|
|
495
|
+
analysis_gaps.append({
|
|
496
|
+
"area": "stack",
|
|
497
|
+
"reason": f"Heuristic-only detection (no manifest): {', '.join(s.stack for s in heuristic_stacks)}",
|
|
498
|
+
"impact": "medium",
|
|
499
|
+
})
|
|
500
|
+
if not sm.dependency_summary or not sm.dependency_summary.requested:
|
|
501
|
+
analysis_gaps.append({
|
|
502
|
+
"area": "dependencies",
|
|
503
|
+
"reason": "Dependencies not analyzed — add --dependencies for full context",
|
|
504
|
+
"impact": "medium",
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
if analysis_gaps:
|
|
508
|
+
result["analysis_gaps"] = analysis_gaps
|
|
454
509
|
|
|
455
510
|
return result
|
|
456
511
|
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=Dh82QypoDBYj3Gur3QVPfjnJcgmk8S2TvsTDOr0Z0Mc,100
|
|
2
2
|
sourcecode/architecture_analyzer.py,sha256=zxgqeqn2FhIUDn06X-whjGKiXbrHFkRlzZcr2zKd0Gw,16874
|
|
3
3
|
sourcecode/architecture_summary.py,sha256=rr-Q2LLe6bXZ27NuV2pYb0_LZyEFn3sngKMdXCZSGbE,13263
|
|
4
4
|
sourcecode/classifier.py,sha256=Ft_RfYS-KOe0t7vjgUx04OoCJd1-DXK7k9-I0CFDSnU,6934
|
|
5
|
-
sourcecode/cli.py,sha256=
|
|
5
|
+
sourcecode/cli.py,sha256=SyO5JHR0cKMoMEas-of1pnWneZMjr7JhTxURR7-Kv5w,30925
|
|
6
6
|
sourcecode/code_notes_analyzer.py,sha256=rRd8bFYV0krjlxxQV0wenwE9K7pVpUQSR7KvSvUQKw4,9226
|
|
7
|
+
sourcecode/confidence_analyzer.py,sha256=gvQ92XCeGsACskSGU-GWTB7OqU1tT0eTgV-A7XA_lGs,8968
|
|
7
8
|
sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
|
|
8
|
-
sourcecode/dependency_analyzer.py,sha256=
|
|
9
|
+
sourcecode/dependency_analyzer.py,sha256=Exq0BfInvfS5iAg9xAr6WI2uPNuotkIudTKcYJcRhB8,52757
|
|
9
10
|
sourcecode/doc_analyzer.py,sha256=Ec3orx6vBKsh5cNM3-F4y2Got2KuKx8w3dErwtdtM-A,19891
|
|
10
11
|
sourcecode/env_analyzer.py,sha256=JZxBOuIxnM0xw0IaJFL6LiPJBErL848L3XoTOHGBqZI,13554
|
|
11
12
|
sourcecode/git_analyzer.py,sha256=S9PGt8RDasBQYQUsZh9-H_KWVlPvzwR4jgM7TKN9ZCk,7643
|
|
12
13
|
sourcecode/graph_analyzer.py,sha256=cxl7Xb88aGxIVOCsatZJAE1CtgXGzIbJfqytV-9kkFs,45172
|
|
13
14
|
sourcecode/metrics_analyzer.py,sha256=4uh11v-Q0gdrN87BOxuFWUym3N3AOkOuy21K5N8peB8,20126
|
|
14
|
-
sourcecode/prepare_context.py,sha256=
|
|
15
|
+
sourcecode/prepare_context.py,sha256=1Z8UcefgIWu1J_tYXVIu2qNbTt08S72gW7QUe9c7u0s,27561
|
|
15
16
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
16
17
|
sourcecode/scanner.py,sha256=aM3h9-DCQ3xKpeHpHYdo2vX6T5P95HA_YwZbkAVNwmo,8288
|
|
17
|
-
sourcecode/schema.py,sha256=
|
|
18
|
+
sourcecode/schema.py,sha256=u8WorLBlbcG-I5oaB0ShTGHDoNaLcl4vyADNV6P2O9g,17159
|
|
18
19
|
sourcecode/semantic_analyzer.py,sha256=asQfJf-EhzYaOTA-iMuZsrVXtbW7SV2WEKCxgsxa88Y,79413
|
|
19
|
-
sourcecode/serializer.py,sha256
|
|
20
|
+
sourcecode/serializer.py,sha256=-dNeUFingePa3WcCTjLe0o6jSdLu4PfL9rPlD8uuky4,25163
|
|
20
21
|
sourcecode/summarizer.py,sha256=YdIA5gGEc0XSctPgmTnFnuZI72bprrOYhkJNoDmvHwM,12114
|
|
21
22
|
sourcecode/tree_utils.py,sha256=Fj9OIuUksBvgibNd3feog0sMDjVypJzPexp5lvMoYWI,1424
|
|
22
23
|
sourcecode/workspace.py,sha256=fQlVoNx8S-fSHpKoJ0JBvEHCFkxszH0KZVJed1i3TRk,6845
|
|
@@ -26,20 +27,20 @@ sourcecode/detectors/dart.py,sha256=QbqaL5v18-_ort75HihVBt8MsKUfOcFDF8IpWFLiXpI,
|
|
|
26
27
|
sourcecode/detectors/dotnet.py,sha256=UC-74VW850r3admcl36R-rl3M0-nBFXw8ph5e9RSas8,1978
|
|
27
28
|
sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJE,1713
|
|
28
29
|
sourcecode/detectors/go.py,sha256=KehAyvC3H_s8zwwqFM0bAbztQbbUBDjpNbGvM2kQlk8,1836
|
|
29
|
-
sourcecode/detectors/heuristic.py,sha256=
|
|
30
|
+
sourcecode/detectors/heuristic.py,sha256=JWb_dXNI1YDxk9a3DFeXTbKYfAGOFqV6jVxxqouWSiA,2238
|
|
30
31
|
sourcecode/detectors/java.py,sha256=qFVpMy5mu490ENhutO1LV2Gidms4ZZUfNX5Qv5Qxh2I,7188
|
|
31
32
|
sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
|
|
32
33
|
sourcecode/detectors/nodejs.py,sha256=qNJ1m0E_TsPiR_iNcSgK4igb4mm99h-HDKE7C8gME6o,4762
|
|
33
34
|
sourcecode/detectors/parsers.py,sha256=ugPg8yNUf0Ai1gA7Fnn6wAkYGFjTxRodSP3IeViYJJ4,2290
|
|
34
35
|
sourcecode/detectors/php.py,sha256=W_AQD0WMVDdWHa9h_ilX6W8XSpz0X4ctpMK2WXfXf1I,1887
|
|
35
36
|
sourcecode/detectors/project.py,sha256=YCynNC8drJutPMiDGPutlYaL49IO8M-1Ms_RwVY09ss,6221
|
|
36
|
-
sourcecode/detectors/python.py,sha256=
|
|
37
|
+
sourcecode/detectors/python.py,sha256=GLuLxJw-6tTrnC1Ng8NLDtt8AOT7waM1MxVWc9jBaEw,9846
|
|
37
38
|
sourcecode/detectors/ruby.py,sha256=Q4B5ePAw6-T4DLfanKJiuLHLqUigTPVrzylcXJMei3M,1591
|
|
38
39
|
sourcecode/detectors/rust.py,sha256=V23NO6Y8NsrhrUlGC2feUEcNK63hf3gc1y3jI4tes1Q,2264
|
|
39
40
|
sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5TcqQ,1627
|
|
40
41
|
sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
|
|
41
42
|
sourcecode/detectors/tooling.py,sha256=hIvop80No22pqyGVJ32NKliSdjkHRePQkIRroqG01bY,1875
|
|
42
|
-
sourcecode-0.
|
|
43
|
-
sourcecode-0.
|
|
44
|
-
sourcecode-0.
|
|
45
|
-
sourcecode-0.
|
|
43
|
+
sourcecode-0.23.0.dist-info/METADATA,sha256=EwoOCYdt8LinpV5ymkOsnd3A01aEcFxbeidutgfwPsw,30326
|
|
44
|
+
sourcecode-0.23.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
45
|
+
sourcecode-0.23.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
|
|
46
|
+
sourcecode-0.23.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|