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.
Files changed (114) hide show
  1. {sourcecode-0.22.0 → sourcecode-0.24.0}/PKG-INFO +1 -1
  2. {sourcecode-0.22.0 → sourcecode-0.24.0}/docs/schema.md +2 -2
  3. {sourcecode-0.22.0 → sourcecode-0.24.0}/pyproject.toml +1 -1
  4. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/architecture_summary.py +50 -0
  6. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/cli.py +37 -5
  7. sourcecode-0.24.0/src/sourcecode/confidence_analyzer.py +190 -0
  8. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/dependency_analyzer.py +51 -5
  9. sourcecode-0.24.0/src/sourcecode/detectors/csproj_parser.py +203 -0
  10. sourcecode-0.24.0/src/sourcecode/detectors/dotnet.py +182 -0
  11. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/heuristic.py +9 -1
  12. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/python.py +6 -0
  13. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/graph_analyzer.py +80 -0
  14. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/prepare_context.py +216 -13
  15. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/schema.py +30 -1
  16. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/serializer.py +108 -53
  17. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_dependency_analyzer_node_python.py +1 -1
  18. sourcecode-0.24.0/tests/test_detector_dotnet.py +482 -0
  19. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detectors_base.py +2 -0
  20. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration.py +5 -2
  21. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_dependencies.py +1 -1
  22. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_schema.py +13 -11
  23. sourcecode-0.24.0/tests/test_signal_hierarchy.py +524 -0
  24. sourcecode-0.22.0/src/sourcecode/detectors/dotnet.py +0 -57
  25. {sourcecode-0.22.0 → sourcecode-0.24.0}/.gitignore +0 -0
  26. {sourcecode-0.22.0 → sourcecode-0.24.0}/.ruff.toml +0 -0
  27. {sourcecode-0.22.0 → sourcecode-0.24.0}/README.md +0 -0
  28. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/architecture_analyzer.py +0 -0
  29. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/classifier.py +0 -0
  30. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  31. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/coverage_parser.py +0 -0
  32. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/__init__.py +0 -0
  33. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/base.py +0 -0
  34. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/dart.py +0 -0
  35. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/elixir.py +0 -0
  36. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/go.py +0 -0
  37. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/java.py +0 -0
  38. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  39. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/nodejs.py +0 -0
  40. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/parsers.py +0 -0
  41. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/php.py +0 -0
  42. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/project.py +0 -0
  43. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/ruby.py +0 -0
  44. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/rust.py +0 -0
  45. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/systems.py +0 -0
  46. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/terraform.py +0 -0
  47. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/detectors/tooling.py +0 -0
  48. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/doc_analyzer.py +0 -0
  49. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/env_analyzer.py +0 -0
  50. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/git_analyzer.py +0 -0
  51. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/metrics_analyzer.py +0 -0
  52. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/redactor.py +0 -0
  53. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/scanner.py +0 -0
  54. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/semantic_analyzer.py +0 -0
  55. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/summarizer.py +0 -0
  56. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/tree_utils.py +0 -0
  57. {sourcecode-0.22.0 → sourcecode-0.24.0}/src/sourcecode/workspace.py +0 -0
  58. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/__init__.py +0 -0
  59. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/conftest.py +0 -0
  60. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/coverage.xml +0 -0
  61. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  62. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  63. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  64. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/go_service/go.mod +0 -0
  65. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/jacoco.xml +0 -0
  66. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/lcov.info +0 -0
  67. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  68. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/nextjs_app/package.json +0 -0
  69. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  70. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  71. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  72. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  73. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  74. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  75. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_architecture_analyzer.py +0 -0
  76. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_architecture_summary.py +0 -0
  77. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_classifier.py +0 -0
  78. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_cli.py +0 -0
  79. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_code_notes_analyzer.py +0 -0
  80. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_coverage_parser.py +0 -0
  81. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_cross_consistency.py +0 -0
  82. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  83. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_dependency_schema.py +0 -0
  84. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_go_rust_java.py +0 -0
  85. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_nodejs.py +0 -0
  86. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_php_ruby_dart.py +0 -0
  87. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_python.py +0 -0
  88. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_universal_managed.py +0 -0
  89. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_detector_universal_systems.py +0 -0
  90. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  91. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_doc_analyzer_python.py +0 -0
  92. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  93. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_graph_analyzer_python_node.py +0 -0
  94. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_graph_schema.py +0 -0
  95. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_detection.py +0 -0
  96. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_docs.py +0 -0
  97. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_graph_modules.py +0 -0
  98. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_lqn.py +0 -0
  99. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_metrics.py +0 -0
  100. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_multistack.py +0 -0
  101. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_semantics.py +0 -0
  102. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_integration_universal.py +0 -0
  103. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_metrics_analyzer.py +0 -0
  104. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_packaging.py +0 -0
  105. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_real_projects.py +0 -0
  106. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_redactor.py +0 -0
  107. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_scanner.py +0 -0
  108. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_schema_normalization.py +0 -0
  109. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_semantic_analyzer_node.py +0 -0
  110. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_semantic_analyzer_python.py +0 -0
  111. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_semantic_import_resolution.py +0 -0
  112. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_semantic_schema.py +0 -0
  113. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_summarizer.py +0 -0
  114. {sourcecode-0.22.0 → sourcecode-0.24.0}/tests/test_workspace_analyzer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 0.22.0
3
+ Version: 0.24.0
4
4
  Summary: Genera un mapa de contexto estructurado de proyectos de software para agentes IA
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -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.22.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.22.0",
648
+ "sourcecode_version": "0.24.0",
649
649
  "analyzed_path": "/abs/path/to/project"
650
650
  },
651
651
  "file_tree": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "0.22.0"
7
+ version = "0.24.0"
8
8
  description = "Genera un mapa de contexto estructurado de proyectos de software para agentes IA"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Genera mapas de contexto estructurado para agentes IA."""
2
2
 
3
- __version__ = "0.22.0"
3
+ __version__ = "0.24.0"
@@ -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
- """Prepare task-aware context optimized for LLM reasoning.
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
- Note: PATH must be provided before the subcommand (default: '.'):
716
+ Examples:
696
717
  sourcecode . prepare-context explain
697
718
  sourcecode . prepare-context fix-bug --path /my/project
698
- sourcecode . prepare-context generate-tests --llm-prompt
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", "webpack", "rollup",
43
- "parcel", "@babel/core", "husky", "lint-staged", "nodemon", "ts-node", "tsx",
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
- "testtool": 3,
53
- "devtool": 4,
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"