sourcecode 0.20.0__tar.gz → 0.22.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 (110) hide show
  1. {sourcecode-0.20.0 → sourcecode-0.22.0}/PKG-INFO +70 -1
  2. {sourcecode-0.20.0 → sourcecode-0.22.0}/README.md +69 -0
  3. {sourcecode-0.20.0 → sourcecode-0.22.0}/docs/schema.md +2 -2
  4. {sourcecode-0.20.0 → sourcecode-0.22.0}/pyproject.toml +1 -1
  5. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/architecture_summary.py +72 -23
  7. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/classifier.py +3 -1
  8. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/cli.py +123 -41
  9. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/dependency_analyzer.py +75 -0
  10. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/base.py +2 -0
  11. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/heuristic.py +2 -0
  12. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/nodejs.py +6 -1
  13. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/project.py +10 -1
  14. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/python.py +63 -14
  15. sourcecode-0.22.0/src/sourcecode/prepare_context.py +486 -0
  16. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/scanner.py +58 -2
  17. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/schema.py +1 -0
  18. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/semantic_analyzer.py +2 -0
  19. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/serializer.py +187 -0
  20. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/summarizer.py +25 -10
  21. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/tree_utils.py +14 -0
  22. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_architecture_summary.py +2 -2
  23. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_dependency_analyzer_node_python.py +2 -2
  24. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_integration.py +12 -5
  25. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_integration_dependencies.py +5 -5
  26. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_integration_docs.py +2 -2
  27. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_integration_graph_modules.py +2 -6
  28. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_integration_lqn.py +6 -8
  29. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_integration_metrics.py +7 -7
  30. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_real_projects.py +1 -1
  31. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_summarizer.py +1 -2
  32. sourcecode-0.20.0/src/sourcecode/prepare_context.py +0 -197
  33. {sourcecode-0.20.0 → sourcecode-0.22.0}/.gitignore +0 -0
  34. {sourcecode-0.20.0 → sourcecode-0.22.0}/.ruff.toml +0 -0
  35. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/architecture_analyzer.py +0 -0
  36. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  37. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/coverage_parser.py +0 -0
  38. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/__init__.py +0 -0
  39. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/dart.py +0 -0
  40. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/dotnet.py +0 -0
  41. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/elixir.py +0 -0
  42. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/go.py +0 -0
  43. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/java.py +0 -0
  44. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  45. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/parsers.py +0 -0
  46. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/php.py +0 -0
  47. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/ruby.py +0 -0
  48. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/rust.py +0 -0
  49. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/systems.py +0 -0
  50. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/terraform.py +0 -0
  51. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/detectors/tooling.py +0 -0
  52. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/doc_analyzer.py +0 -0
  53. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/env_analyzer.py +0 -0
  54. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/git_analyzer.py +0 -0
  55. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/graph_analyzer.py +0 -0
  56. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/metrics_analyzer.py +0 -0
  57. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/redactor.py +0 -0
  58. {sourcecode-0.20.0 → sourcecode-0.22.0}/src/sourcecode/workspace.py +0 -0
  59. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/__init__.py +0 -0
  60. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/conftest.py +0 -0
  61. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/coverage.xml +0 -0
  62. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  63. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  64. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  65. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/go_service/go.mod +0 -0
  66. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/jacoco.xml +0 -0
  67. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/lcov.info +0 -0
  68. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  69. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/nextjs_app/package.json +0 -0
  70. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  71. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  72. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  73. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  74. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  75. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  76. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_architecture_analyzer.py +0 -0
  77. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_classifier.py +0 -0
  78. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_cli.py +0 -0
  79. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_code_notes_analyzer.py +0 -0
  80. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_coverage_parser.py +0 -0
  81. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_cross_consistency.py +0 -0
  82. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  83. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_dependency_schema.py +0 -0
  84. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_detector_go_rust_java.py +0 -0
  85. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_detector_nodejs.py +0 -0
  86. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_detector_php_ruby_dart.py +0 -0
  87. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_detector_python.py +0 -0
  88. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_detector_universal_managed.py +0 -0
  89. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_detector_universal_systems.py +0 -0
  90. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_detectors_base.py +0 -0
  91. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  92. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_doc_analyzer_python.py +0 -0
  93. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  94. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_graph_analyzer_python_node.py +0 -0
  95. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_graph_schema.py +0 -0
  96. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_integration_detection.py +0 -0
  97. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_integration_multistack.py +0 -0
  98. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_integration_semantics.py +0 -0
  99. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_integration_universal.py +0 -0
  100. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_metrics_analyzer.py +0 -0
  101. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_packaging.py +0 -0
  102. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_redactor.py +0 -0
  103. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_scanner.py +0 -0
  104. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_schema.py +0 -0
  105. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_schema_normalization.py +0 -0
  106. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_semantic_analyzer_node.py +0 -0
  107. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_semantic_analyzer_python.py +0 -0
  108. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_semantic_import_resolution.py +0 -0
  109. {sourcecode-0.20.0 → sourcecode-0.22.0}/tests/test_semantic_schema.py +0 -0
  110. {sourcecode-0.20.0 → sourcecode-0.22.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.20.0
3
+ Version: 0.22.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
@@ -636,6 +636,75 @@ Combine flags for a comprehensive project handoff:
636
636
  sourcecode --compact --env-map --code-notes --git-context .
637
637
  ```
638
638
 
639
+ ## `prepare-context` — Task-aware context for LLMs
640
+
641
+ `prepare-context` is a subcommand that builds a focused, task-specific context optimized for LLM reasoning. Instead of a full project dump, it returns only the data an LLM needs for a specific goal.
642
+
643
+ > **Note:** Because `sourcecode` has a positional `PATH` argument, you must provide it explicitly before the subcommand:
644
+ > ```bash
645
+ > sourcecode . prepare-context <task> # current directory
646
+ > sourcecode /my/project prepare-context <task>
647
+ > ```
648
+
649
+ ### Available tasks
650
+
651
+ | Task | Goal | Output |
652
+ |------|------|--------|
653
+ | `explain` | Onboard an LLM to the project | `project_summary`, `architecture_summary`, `relevant_files`, `key_dependencies` |
654
+ | `fix-bug` | Identify likely bug locations | `relevant_files` (ranked by risk), `suspected_areas`, `code_notes_summary` |
655
+ | `refactor` | Surface improvement opportunities | `relevant_files`, `improvement_opportunities`, `architecture_summary` |
656
+ | `generate-tests` | Find untested areas | `test_gaps`, `relevant_files` (source without tests), `key_dependencies` |
657
+
658
+ ### Usage
659
+
660
+ ```bash
661
+ # List tasks with descriptions
662
+ sourcecode . prepare-context --task-help
663
+
664
+ # Explain the project
665
+ sourcecode . prepare-context explain
666
+
667
+ # Find bug areas, with a ready-to-paste LLM prompt
668
+ sourcecode . prepare-context fix-bug --llm-prompt
669
+
670
+ # Find untested files in a specific project
671
+ sourcecode . prepare-context generate-tests --path /my/project
672
+
673
+ # Preview what will be analyzed (no analysis run)
674
+ sourcecode . prepare-context refactor --dry-run
675
+ ```
676
+
677
+ ### Output format
678
+
679
+ ```json
680
+ {
681
+ "task": "fix-bug",
682
+ "goal": "Identify the most likely files and areas where a bug may be located.",
683
+ "project_summary": "CLI en Python (Typer). Entry points: src/cli.py. 4 dependencias.",
684
+ "architecture_summary": null,
685
+ "relevant_files": [
686
+ { "path": "src/handler.py", "role": "source", "score": 2.0, "reason": "matches 'handler'" },
687
+ { "path": "src/cli.py", "role": "entrypoint", "score": 3.0, "reason": "entry point" }
688
+ ],
689
+ "suspected_areas": ["src/handler.py (2 annotations)", "src/parser.py (1 annotation)"],
690
+ "code_notes_summary": { "total": 5, "by_kind": { "FIXME": 3, "BUG": 2 }, "top_files": ["src/handler.py"] }
691
+ }
692
+ ```
693
+
694
+ ### `--llm-prompt`
695
+
696
+ Add `--llm-prompt` to include a ready-to-use prompt that you can paste directly into any LLM:
697
+
698
+ ```bash
699
+ sourcecode . prepare-context explain --llm-prompt | jq -r '.llm_prompt'
700
+ ```
701
+
702
+ The prompt is task-specific and includes the project context, relevant files, and concrete instructions for the LLM.
703
+
704
+ ### Task auto-selection with `--agent`
705
+
706
+ For zero-config usage, `--agent` on the main command automatically selects the right flags for the project type. For task-aware context, use `prepare-context` explicitly with the task that matches your goal.
707
+
639
708
  ## Development
640
709
 
641
710
  Editable install with development dependencies:
@@ -620,6 +620,75 @@ Combine flags for a comprehensive project handoff:
620
620
  sourcecode --compact --env-map --code-notes --git-context .
621
621
  ```
622
622
 
623
+ ## `prepare-context` — Task-aware context for LLMs
624
+
625
+ `prepare-context` is a subcommand that builds a focused, task-specific context optimized for LLM reasoning. Instead of a full project dump, it returns only the data an LLM needs for a specific goal.
626
+
627
+ > **Note:** Because `sourcecode` has a positional `PATH` argument, you must provide it explicitly before the subcommand:
628
+ > ```bash
629
+ > sourcecode . prepare-context <task> # current directory
630
+ > sourcecode /my/project prepare-context <task>
631
+ > ```
632
+
633
+ ### Available tasks
634
+
635
+ | Task | Goal | Output |
636
+ |------|------|--------|
637
+ | `explain` | Onboard an LLM to the project | `project_summary`, `architecture_summary`, `relevant_files`, `key_dependencies` |
638
+ | `fix-bug` | Identify likely bug locations | `relevant_files` (ranked by risk), `suspected_areas`, `code_notes_summary` |
639
+ | `refactor` | Surface improvement opportunities | `relevant_files`, `improvement_opportunities`, `architecture_summary` |
640
+ | `generate-tests` | Find untested areas | `test_gaps`, `relevant_files` (source without tests), `key_dependencies` |
641
+
642
+ ### Usage
643
+
644
+ ```bash
645
+ # List tasks with descriptions
646
+ sourcecode . prepare-context --task-help
647
+
648
+ # Explain the project
649
+ sourcecode . prepare-context explain
650
+
651
+ # Find bug areas, with a ready-to-paste LLM prompt
652
+ sourcecode . prepare-context fix-bug --llm-prompt
653
+
654
+ # Find untested files in a specific project
655
+ sourcecode . prepare-context generate-tests --path /my/project
656
+
657
+ # Preview what will be analyzed (no analysis run)
658
+ sourcecode . prepare-context refactor --dry-run
659
+ ```
660
+
661
+ ### Output format
662
+
663
+ ```json
664
+ {
665
+ "task": "fix-bug",
666
+ "goal": "Identify the most likely files and areas where a bug may be located.",
667
+ "project_summary": "CLI en Python (Typer). Entry points: src/cli.py. 4 dependencias.",
668
+ "architecture_summary": null,
669
+ "relevant_files": [
670
+ { "path": "src/handler.py", "role": "source", "score": 2.0, "reason": "matches 'handler'" },
671
+ { "path": "src/cli.py", "role": "entrypoint", "score": 3.0, "reason": "entry point" }
672
+ ],
673
+ "suspected_areas": ["src/handler.py (2 annotations)", "src/parser.py (1 annotation)"],
674
+ "code_notes_summary": { "total": 5, "by_kind": { "FIXME": 3, "BUG": 2 }, "top_files": ["src/handler.py"] }
675
+ }
676
+ ```
677
+
678
+ ### `--llm-prompt`
679
+
680
+ Add `--llm-prompt` to include a ready-to-use prompt that you can paste directly into any LLM:
681
+
682
+ ```bash
683
+ sourcecode . prepare-context explain --llm-prompt | jq -r '.llm_prompt'
684
+ ```
685
+
686
+ The prompt is task-specific and includes the project context, relevant files, and concrete instructions for the LLM.
687
+
688
+ ### Task auto-selection with `--agent`
689
+
690
+ For zero-config usage, `--agent` on the main command automatically selects the right flags for the project type. For task-aware context, use `prepare-context` explicitly with the task that matches your goal.
691
+
623
692
  ## Development
624
693
 
625
694
  Editable install with development dependencies:
@@ -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.20.0",
84
+ "sourcecode_version": "0.22.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.20.0",
648
+ "sourcecode_version": "0.22.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.20.0"
7
+ version = "0.22.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.20.0"
3
+ __version__ = "0.22.0"
@@ -14,6 +14,20 @@ _NODE_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
14
14
  _GO_EXTENSIONS = {".go"}
15
15
  _JAVA_EXTENSIONS = {".java", ".kt", ".scala"}
16
16
 
17
+ _CORE_DETECTION_MODULES = {"scanner", "detectors", "classifier", "workspace"}
18
+
19
+ _OPTIONAL_LABEL_MAP: dict[str, str] = {
20
+ "DependencyAnalyzer": "dependencias",
21
+ "GraphAnalyzer": "grafo de módulos",
22
+ "DocAnalyzer": "docs y docstrings",
23
+ "MetricsAnalyzer": "métricas de calidad",
24
+ "SemanticAnalyzer": "semántica y call graph",
25
+ "ArchitectureAnalyzer": "inferencia arquitectónica",
26
+ "GitAnalyzer": "contexto git",
27
+ "EnvAnalyzer": "variables de entorno",
28
+ "CodeNotesAnalyzer": "anotaciones de código",
29
+ }
30
+
17
31
 
18
32
  class ArchitectureSummarizer:
19
33
  """Construye un resumen arquitectonico estatico de 3-5 lineas."""
@@ -51,18 +65,25 @@ class ArchitectureSummarizer:
51
65
  if content is None:
52
66
  return f"Entry point principal: {entry_point.path}. Arquitectura no inferida con suficiente evidencia estatica."
53
67
 
54
- lines = [self._describe_entry_point(entry_point, sm.project_type)]
55
68
  suffix = Path(entry_point.path).suffix
56
69
  if suffix in _PYTHON_EXTENSIONS:
57
- lines.extend(self._summarize_python_entry(entry_point.path, content))
70
+ lang_lines = self._summarize_python_entry(entry_point.path, content)
58
71
  elif suffix in _NODE_EXTENSIONS:
59
- lines.extend(self._summarize_node_entry(entry_point.path, content))
72
+ lang_lines = self._summarize_node_entry(entry_point.path, content)
60
73
  elif suffix in _GO_EXTENSIONS:
61
- lines.extend(self._summarize_go_entry(entry_point.path, content))
74
+ lang_lines = self._summarize_go_entry(entry_point.path, content)
62
75
  elif suffix in _JAVA_EXTENSIONS:
63
- lines.extend(self._summarize_java_entry(entry_point.path, content, sm.stacks))
76
+ lang_lines = self._summarize_java_entry(entry_point.path, content, sm.stacks)
64
77
  else:
65
- lines.append("Orquesta modulos internos no detallados por el analisis estatico disponible.")
78
+ lang_lines = []
79
+
80
+ if lang_lines:
81
+ # Product-level description available — no need for internal "Entry point: ..." header
82
+ lines = lang_lines
83
+ else:
84
+ lines = [self._describe_entry_point(entry_point, sm.project_type)]
85
+ if not lang_lines:
86
+ lines.append("Orquesta modulos internos no detallados por el analisis estatico disponible.")
66
87
 
67
88
  unique_lines: list[str] = []
68
89
  seen: set[str] = set()
@@ -78,11 +99,11 @@ class ArchitectureSummarizer:
78
99
  try:
79
100
  tree = ast.parse(content)
80
101
  except SyntaxError:
81
- return ["Orquesta modulos Python, pero el entry point no pudo parsearse por completo."]
102
+ return []
82
103
 
83
104
  package_prefix = self._python_package_prefix(path)
84
105
  imported_modules: list[str] = []
85
- optional_analyzers: list[str] = []
106
+ optional_class_names: list[str] = []
86
107
  uses_serializer = False
87
108
  uses_redactor = False
88
109
 
@@ -90,30 +111,58 @@ class ArchitectureSummarizer:
90
111
  if isinstance(node, ast.ImportFrom) and isinstance(node.module, str):
91
112
  if package_prefix and node.module.startswith(package_prefix):
92
113
  imported_modules.append(node.module.rsplit(".", 1)[-1])
93
- if node.module == "sourcecode.serializer":
114
+ if node.module.endswith(".serializer") or "serializer" in node.module:
94
115
  uses_serializer = True
95
- if node.module == "sourcecode.redactor":
116
+ if node.module.endswith(".redactor") or "redactor" in node.module:
96
117
  uses_redactor = True
97
118
  elif isinstance(node, ast.Assign):
98
- optional = self._extract_optional_analyzer(node)
99
- if optional:
100
- optional_analyzers.append(optional)
119
+ cls_name = self._extract_optional_analyzer_class_name(node)
120
+ if cls_name:
121
+ optional_class_names.append(cls_name)
101
122
 
123
+ module_set = set(imported_modules)
102
124
  lines: list[str] = []
103
- core_modules = self._format_module_list(imported_modules)
104
- if core_modules:
105
- lines.append(f"Orquesta modulos internos: {core_modules}.")
106
- if optional_analyzers:
107
- lines.append(f"Activa analisis opcionales: {', '.join(optional_analyzers)}.")
125
+
126
+ # Detection line — infer from core modules present
127
+ has_core = bool(module_set & _CORE_DETECTION_MODULES)
128
+ if has_core:
129
+ lines.append("Analiza el árbol del repositorio y detecta stack, entrypoints y tipo de proyecto.")
130
+ elif module_set:
131
+ lines.append("Analiza el proyecto y produce información estructurada.")
132
+
133
+ # Output line
108
134
  if uses_serializer:
109
- output_line = "Produce un SourceMap serializado en JSON/YAML y una vista compacta."
135
+ out = "Produce un SourceMap serializable en JSON/YAML"
110
136
  if uses_redactor:
111
- output_line = "Produce un SourceMap serializado en JSON/YAML y una vista compacta con redaccion de secretos."
112
- lines.append(output_line)
113
- elif not lines:
114
- lines.append("Orquesta modulos Python internos sin un patron de salida claramente inferible.")
137
+ out += " con redacción de secretos"
138
+ lines.append(out + ".")
139
+
140
+ # Optional capabilities line
141
+ opt_labels = [
142
+ _OPTIONAL_LABEL_MAP[cls]
143
+ for cls in optional_class_names
144
+ if cls in _OPTIONAL_LABEL_MAP
145
+ ]
146
+ if opt_labels:
147
+ if len(opt_labels) > 1:
148
+ joined = ", ".join(opt_labels[:-1]) + " y " + opt_labels[-1]
149
+ else:
150
+ joined = opt_labels[0]
151
+ lines.append(f"Opcionalmente añade {joined}.")
152
+
115
153
  return lines
116
154
 
155
+ def _extract_optional_analyzer_class_name(self, node: ast.Assign) -> str | None:
156
+ value = node.value
157
+ if not isinstance(value, ast.IfExp):
158
+ return None
159
+ if not isinstance(value.body, ast.Call):
160
+ return None
161
+ if not isinstance(value.body.func, ast.Name):
162
+ return None
163
+ cls_name = value.body.func.id
164
+ return cls_name if cls_name.endswith("Analyzer") else None
165
+
117
166
  def _summarize_node_entry(self, path: str, content: str) -> list[str]:
118
167
  imports = re.findall(r"""from\s+['"](\.?\.?/[^'"]+)['"]|require\(['"](\.?\.?/[^'"]+)['"]\)""", content)
119
168
  modules = [item for pair in imports for item in pair if item]
@@ -128,7 +128,9 @@ class TypeClassifier:
128
128
  has_api = False
129
129
  for stack in stacks:
130
130
  frameworks = {framework.name for framework in stack.frameworks}
131
- if frameworks & _WEB_FRAMEWORKS or stack.stack == "nodejs":
131
+ if frameworks & _WEB_FRAMEWORKS:
132
+ has_web = True
133
+ elif stack.stack == "nodejs" and stack.detection_method != "heuristic":
132
134
  has_web = True
133
135
  if frameworks & _API_FRAMEWORKS or stack.stack in _API_STACKS:
134
136
  has_api = True
@@ -78,7 +78,12 @@ def main(
78
78
  no_tree: bool = typer.Option(
79
79
  False,
80
80
  "--no-tree",
81
- help="Suprimir file_tree y file_paths del output (ideal con --dependencies en proyectos grandes)",
81
+ help="Suprimir file_tree y file_paths del output (ahora deprecado: el arbol ya no se incluye por defecto)",
82
+ ),
83
+ tree: bool = typer.Option(
84
+ False,
85
+ "--tree",
86
+ help="Incluir file_tree completo y file_paths en el output (capa deep-dive)",
82
87
  ),
83
88
  no_redact: bool = typer.Option(
84
89
  False,
@@ -114,7 +119,7 @@ def main(
114
119
  full_metrics: bool = typer.Option(
115
120
  False,
116
121
  "--full-metrics",
117
- help="Incluir metricas de calidad: LOC, simbolos, complejidad, tests y cobertura por fichero",
122
+ help="Auditoria tecnica: LOC, simbolos, complejidad ciclomatica y cobertura por fichero. No incluido en --agent (uso: CI, code review, no context principal para agentes IA)",
118
123
  ),
119
124
  semantics: bool = typer.Option(
120
125
  False,
@@ -159,10 +164,14 @@ def main(
159
164
  agent: bool = typer.Option(
160
165
  False,
161
166
  "--agent",
162
- help="Modo agente: selecciona automaticamente --compact --dependencies --env-map --code-notes y activa --no-tree en proyectos Java/Gradle o grandes",
167
+ help="Modo agente: output estructurado y sin ruido para consumo por IA. Incluye identidad, entrypoints, arquitectura, dependencias clave, señales operacionales y gaps. Sin arbol de ficheros ni secciones vacias.",
163
168
  ),
164
169
  ) -> None:
165
170
  """Genera un mapa de contexto estructurado del proyecto en formato JSON o YAML."""
171
+ # When a subcommand (e.g. prepare-context) is invoked, skip the main analysis.
172
+ if ctx.invoked_subcommand is not None:
173
+ return
174
+
166
175
  # Validar formato
167
176
  if format not in FORMAT_CHOICES:
168
177
  typer.echo(
@@ -212,7 +221,7 @@ def main(
212
221
  SourceMap,
213
222
  StackDetection,
214
223
  )
215
- from sourcecode.serializer import compact_view, normalize_source_map, validate_cross_analyzer_consistency, validate_source_map, write_output
224
+ from sourcecode.serializer import agent_view, compact_view, normalize_source_map, standard_view, validate_cross_analyzer_consistency, validate_source_map, write_output
216
225
  from sourcecode.workspace import WorkspaceAnalyzer
217
226
 
218
227
  # 1. Escanear el directorio (SCAN-01 a SCAN-05)
@@ -231,15 +240,13 @@ def main(
231
240
  _java_min_depth = 8
232
241
  effective_depth = max(depth, _java_min_depth) if _is_java and depth < _java_min_depth else depth
233
242
 
234
- # --agent: auto-select flags based on project characteristics
243
+ # --agent: enable signal analyzers; output via agent_view (not compact)
235
244
  if agent:
236
- compact = True
237
- if _is_java:
238
- no_tree = True
239
- _agent_flags = ["--compact", "--dependencies", "--env-map", "--code-notes"]
240
- if no_tree:
241
- _agent_flags.append("--no-tree")
242
- typer.echo(f"[agent] {' '.join(_agent_flags)}", err=True)
245
+ dependencies = True
246
+ env_map = True
247
+ code_notes = True
248
+ no_tree = True # agents never need the raw file tree
249
+ typer.echo("[agent] dependencies env-map code-notes (no-tree)", err=True)
243
250
 
244
251
  scanner = FileScanner(target, max_depth=effective_depth)
245
252
  raw_tree = scanner.scan_tree()
@@ -558,16 +565,20 @@ def main(
558
565
  p.replace("\\", "/") for p in flatten_file_tree(sm.file_tree)
559
566
  ]
560
567
 
561
- # LQN-05: top-15 dependencias directas de manifest/lockfile
568
+ # LQN-05: top-15 dependencias directas de manifest/lockfile, ordenadas por rol
562
569
  if dependency_analyzer is not None:
570
+ from sourcecode.dependency_analyzer import _ROLE_PRIORITY
571
+
563
572
  primary_ecosystem = sm.stacks[0].stack if sm.stacks else ""
564
573
  direct_deps = [
565
574
  d for d in sm.dependencies
566
575
  if d.scope != "transitive" and d.source in {"manifest", "lockfile"}
567
576
  ]
568
577
 
569
- def _dep_sort_key(d: Any) -> tuple[int, str]:
570
- return (0 if d.ecosystem == primary_ecosystem else 1, d.name.lower())
578
+ def _dep_sort_key(d: Any) -> tuple[int, int, str]:
579
+ role_order = _ROLE_PRIORITY.get(d.role or "runtime", 5)
580
+ eco_order = 0 if d.ecosystem == primary_ecosystem else 1
581
+ return (role_order, eco_order, d.name.lower())
571
582
 
572
583
  sm.key_dependencies = sorted(direct_deps, key=_dep_sort_key)[:15]
573
584
 
@@ -612,24 +623,23 @@ def main(
612
623
  for _finding in validate_cross_analyzer_consistency(sm, strict=False):
613
624
  typer.echo(f"[consistency] {_finding}", err=True)
614
625
 
615
- # 4. Serializar (con o sin modo compact)
616
- if compact:
626
+ # 4. Serializar
627
+ if agent:
628
+ data = agent_view(sm)
629
+ if not no_redact:
630
+ data = redact_dict(data)
631
+ content = json.dumps(data, indent=2, ensure_ascii=False)
632
+ elif compact:
617
633
  data = compact_view(sm, no_tree=no_tree)
618
- # Aplicar redaccion sobre el dict del compact view
619
634
  if not no_redact:
620
635
  data = redact_dict(data)
621
636
  content = json.dumps(data, indent=2, ensure_ascii=False)
622
637
  else:
623
- # Redactar sobre el dict serializado (SEC-01, SEC-03)
624
- raw_dict = asdict(sm)
625
- if no_tree:
626
- raw_dict.pop("file_tree", None)
627
- raw_dict.pop("file_paths", None)
638
+ raw_dict = standard_view(sm, include_tree=tree and not no_tree)
628
639
  if not no_redact:
629
640
  raw_dict = redact_dict(raw_dict)
630
641
 
631
642
  if format == "yaml":
632
- # Para YAML, serializar el dict directamente con ruamel.yaml
633
643
  from io import StringIO
634
644
 
635
645
  from ruamel.yaml import YAML
@@ -654,31 +664,103 @@ def main(
654
664
 
655
665
  @app.command("prepare-context")
656
666
  def prepare_context_cmd(
657
- task: str = typer.Argument(..., help="Descripción de la tarea"),
658
- path: Path = typer.Option(".", "--path", "-p", help="Directorio del proyecto"),
667
+ task: Optional[str] = typer.Argument(
668
+ None,
669
+ help="Task: explain | fix-bug | refactor | generate-tests",
670
+ ),
671
+ path: Path = typer.Option(
672
+ Path("."),
673
+ "--path", "-p",
674
+ help="Project directory to analyze (default: current directory)",
675
+ ),
676
+ llm_prompt: bool = typer.Option(
677
+ False,
678
+ "--llm-prompt",
679
+ help="Append a ready-to-use LLM prompt to the output",
680
+ ),
681
+ task_help: bool = typer.Option(
682
+ False,
683
+ "--task-help",
684
+ help="List available tasks with descriptions and exit",
685
+ ),
686
+ dry_run: bool = typer.Option(
687
+ False,
688
+ "--dry-run",
689
+ help="Show what would be analyzed without running it",
690
+ ),
659
691
  ) -> None:
660
- """Prepara contexto mínimo optimizado para que un LLM modifique el código."""
661
- from dataclasses import asdict
692
+ """Prepare task-aware context optimized for LLM reasoning.
693
+
694
+ \b
695
+ Note: PATH must be provided before the subcommand (default: '.'):
696
+ sourcecode . prepare-context explain
697
+ sourcecode . prepare-context fix-bug --path /my/project
698
+ sourcecode . prepare-context generate-tests --llm-prompt
699
+ sourcecode . prepare-context --task-help
700
+ """
701
+ from sourcecode.prepare_context import TASKS, TaskContextBuilder
702
+
703
+ if task_help:
704
+ typer.echo("Available tasks:\n")
705
+ for name, spec in TASKS.items():
706
+ typer.echo(f" {name:<20} {spec.description}")
707
+ typer.echo(f" {'':20} Output: {spec.output_hint}\n")
708
+ raise typer.Exit()
662
709
 
663
- from sourcecode.prepare_context import ContextBuilder
710
+ if task is None:
711
+ typer.echo(
712
+ f"Error: task is required. Available: {', '.join(TASKS)}\n"
713
+ "Use --task-help for descriptions.",
714
+ err=True,
715
+ )
716
+ raise typer.Exit(code=1)
664
717
 
665
- target = path.resolve()
718
+ if task not in TASKS:
719
+ typer.echo(
720
+ f"Error: unknown task '{task}'. Available: {', '.join(TASKS)}",
721
+ err=True,
722
+ )
723
+ raise typer.Exit(code=1)
666
724
 
725
+ target = path.resolve()
667
726
  if not target.exists() or not target.is_dir():
668
727
  typer.echo(f"Error: '{target}' no es un directorio válido.", err=True)
669
728
  raise typer.Exit(code=1)
670
729
 
671
- builder = ContextBuilder(target)
672
- result = builder.prepare(task)
673
-
674
- out = {
675
- "task": result.task,
676
- "entry_points": result.entry_points,
677
- "relevant_files": [asdict(f) for f in result.relevant_files],
678
- "call_flow": result.call_flow,
679
- "snippets": [asdict(s) for s in result.snippets],
680
- "tests": result.tests,
681
- "notes": result.notes,
730
+ if dry_run:
731
+ spec = TASKS[task]
732
+ typer.echo(f"task: {task}")
733
+ typer.echo(f"goal: {spec.goal}")
734
+ typer.echo(f"path: {target}")
735
+ typer.echo(f"analyzers: dependencies={'yes' if spec.enable_dependencies else 'no'}"
736
+ f", code_notes={'yes' if spec.enable_code_notes else 'no'}")
737
+ typer.echo(f"output: {spec.output_hint}")
738
+ raise typer.Exit()
739
+
740
+ from dataclasses import asdict
741
+
742
+ builder = TaskContextBuilder(target)
743
+ output = builder.build(task)
744
+
745
+ out: dict[str, Any] = {
746
+ "task": output.task,
747
+ "goal": output.goal,
748
+ "project_summary": output.project_summary,
749
+ "architecture_summary": output.architecture_summary,
750
+ "relevant_files": [asdict(f) for f in output.relevant_files],
751
+ "key_dependencies": output.key_dependencies,
682
752
  }
753
+ if output.suspected_areas:
754
+ out["suspected_areas"] = output.suspected_areas
755
+ if output.improvement_opportunities:
756
+ out["improvement_opportunities"] = output.improvement_opportunities
757
+ if output.test_gaps:
758
+ out["test_gaps"] = output.test_gaps
759
+ if output.code_notes_summary:
760
+ out["code_notes_summary"] = output.code_notes_summary
761
+ if output.limitations:
762
+ out["limitations"] = output.limitations
763
+ if llm_prompt:
764
+ out["llm_prompt"] = builder.render_prompt(output)
683
765
 
684
766
  typer.echo(json.dumps(out, indent=2, ensure_ascii=False))