sourcecode 0.22.0__tar.gz → 0.24.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sourcecode-0.22.0 → sourcecode-0.24.0}/PKG-INFO +1 -1
- {sourcecode-0.22.0 → sourcecode-0.24.0}/docs/schema.md +2 -2
- {sourcecode-0.22.0 → sourcecode-0.24.0}/pyproject.toml +1 -1
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/architecture_summary.py +50 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/cli.py +37 -5
- sourcecode-0.24.0/src/sourcecode/confidence_analyzer.py +190 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/dependency_analyzer.py +51 -5
- sourcecode-0.24.0/src/sourcecode/detectors/csproj_parser.py +203 -0
- sourcecode-0.24.0/src/sourcecode/detectors/dotnet.py +182 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/heuristic.py +9 -1
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/python.py +6 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/graph_analyzer.py +80 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/prepare_context.py +216 -13
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/schema.py +30 -1
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/serializer.py +108 -53
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_dependency_analyzer_node_python.py +1 -1
- sourcecode-0.24.0/tests/test_detector_dotnet.py +482 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detectors_base.py +2 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration.py +5 -2
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_dependencies.py +1 -1
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_schema.py +13 -11
- sourcecode-0.24.0/tests/test_signal_hierarchy.py +524 -0
- sourcecode-0.22.0/src/sourcecode/detectors/dotnet.py +0 -57
- {sourcecode-0.22.0 → sourcecode-0.24.0}/.gitignore +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/.ruff.toml +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/README.md +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/__init__.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/conftest.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_architecture_summary.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_classifier.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_cli.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_nodejs.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_python.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_graph_analyzer_polyglot.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_docs.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_graph_modules.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_lqn.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_metrics.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_packaging.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_real_projects.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_redactor.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_scanner.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_summarizer.py +0 -0
- {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_workspace_analyzer.py +0 -0
|
@@ -81,7 +81,7 @@ Campos:
|
|
|
81
81
|
{
|
|
82
82
|
"schema_version": "1.0",
|
|
83
83
|
"generated_at": "2026-04-07T19:41:05.686277+00:00",
|
|
84
|
-
"sourcecode_version": "0.
|
|
84
|
+
"sourcecode_version": "0.24.0",
|
|
85
85
|
"analyzed_path": "/abs/path/to/project"
|
|
86
86
|
}
|
|
87
87
|
```
|
|
@@ -645,7 +645,7 @@ Ejemplo de salida para un monorepo con web Node.js y API Python con `--dependenc
|
|
|
645
645
|
"metadata": {
|
|
646
646
|
"schema_version": "1.0",
|
|
647
647
|
"generated_at": "2026-04-07T19:41:05.686277+00:00",
|
|
648
|
-
"sourcecode_version": "0.
|
|
648
|
+
"sourcecode_version": "0.24.0",
|
|
649
649
|
"analyzed_path": "/abs/path/to/project"
|
|
650
650
|
},
|
|
651
651
|
"file_tree": {
|
|
@@ -74,6 +74,8 @@ class ArchitectureSummarizer:
|
|
|
74
74
|
lang_lines = self._summarize_go_entry(entry_point.path, content)
|
|
75
75
|
elif suffix in _JAVA_EXTENSIONS:
|
|
76
76
|
lang_lines = self._summarize_java_entry(entry_point.path, content, sm.stacks)
|
|
77
|
+
elif suffix in {".cs", ".fs", ".vb"}:
|
|
78
|
+
lang_lines = self._summarize_dotnet_entry(sm.stacks)
|
|
77
79
|
else:
|
|
78
80
|
lang_lines = []
|
|
79
81
|
|
|
@@ -189,6 +191,44 @@ class ArchitectureSummarizer:
|
|
|
189
191
|
lines.append("Orquesta el arranque de la aplicacion JVM.")
|
|
190
192
|
return lines
|
|
191
193
|
|
|
194
|
+
def _summarize_dotnet_entry(self, stacks: list[StackDetection]) -> list[str]:
|
|
195
|
+
dotnet_stacks = [s for s in stacks if s.stack == "dotnet"]
|
|
196
|
+
if not dotnet_stacks:
|
|
197
|
+
return []
|
|
198
|
+
lines: list[str] = []
|
|
199
|
+
signals = [sig for s in dotnet_stacks for sig in s.signals]
|
|
200
|
+
|
|
201
|
+
for sig in signals:
|
|
202
|
+
if "project" in sig and "detected" in sig:
|
|
203
|
+
lines.append(sig[0].upper() + sig[1:] + ".")
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
for sig in signals:
|
|
207
|
+
if sig.startswith("project types:"):
|
|
208
|
+
types = sig.removeprefix("project types:").strip()
|
|
209
|
+
lines.append(f"Stack: {types}.")
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
for sig in signals:
|
|
213
|
+
if sig.startswith("target frameworks:"):
|
|
214
|
+
fws = sig.removeprefix("target frameworks:").strip()
|
|
215
|
+
lines.append(f"Target: {fws}.")
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
for sig in signals:
|
|
219
|
+
if sig.startswith("architecture:"):
|
|
220
|
+
pattern = sig.removeprefix("architecture:").strip()
|
|
221
|
+
lines.append(f"Patrón detectado: {pattern}.")
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
framework_names = [f.name for s in dotnet_stacks for f in s.frameworks]
|
|
225
|
+
if framework_names:
|
|
226
|
+
lines.append(f"Frameworks: {', '.join(framework_names)}.")
|
|
227
|
+
|
|
228
|
+
if not lines:
|
|
229
|
+
lines.append("Solución .NET detectada.")
|
|
230
|
+
return lines
|
|
231
|
+
|
|
192
232
|
def _summarize_go_entry(self, path: str, content: str) -> list[str]:
|
|
193
233
|
imports = re.findall(r'"([^"]+)"', content)
|
|
194
234
|
internal = [module for module in imports if not module.startswith(("fmt", "net/", "os", "context"))]
|
|
@@ -273,6 +313,16 @@ class ArchitectureSummarizer:
|
|
|
273
313
|
confidence="medium",
|
|
274
314
|
)
|
|
275
315
|
)
|
|
316
|
+
elif path.endswith("Program.cs") or path.endswith("Program.fs"):
|
|
317
|
+
candidates.append(
|
|
318
|
+
EntryPoint(
|
|
319
|
+
path=path,
|
|
320
|
+
stack="dotnet",
|
|
321
|
+
kind="cli",
|
|
322
|
+
source="convention",
|
|
323
|
+
confidence="medium",
|
|
324
|
+
)
|
|
325
|
+
)
|
|
276
326
|
return candidates
|
|
277
327
|
|
|
278
328
|
def _fallback_priority(self, path: str) -> tuple[int, int, str]:
|
|
@@ -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"
|