sourcecode 0.28.0__tar.gz → 0.30.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 (136) hide show
  1. sourcecode-0.30.0/.agents/skills/source-command-gsd-join-discord/SKILL.md +24 -0
  2. sourcecode-0.30.0/.agents/skills/source-command-gsd-review-backlog/SKILL.md +63 -0
  3. sourcecode-0.30.0/.agents/skills/source-command-gsd-workstreams/SKILL.md +72 -0
  4. {sourcecode-0.28.0 → sourcecode-0.30.0}/PKG-INFO +1 -1
  5. {sourcecode-0.28.0 → sourcecode-0.30.0}/pyproject.toml +1 -1
  6. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/__init__.py +1 -1
  7. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/architecture_analyzer.py +76 -11
  8. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/architecture_summary.py +4 -8
  9. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/classifier.py +5 -1
  10. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/cli.py +193 -3
  11. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/confidence_analyzer.py +72 -7
  12. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/heuristic.py +19 -1
  13. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/nodejs.py +70 -26
  14. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/project.py +7 -0
  15. sourcecode-0.30.0/src/sourcecode/entrypoint_classifier.py +106 -0
  16. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/env_analyzer.py +25 -13
  17. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/git_analyzer.py +57 -7
  18. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/schema.py +31 -3
  19. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/serializer.py +83 -58
  20. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_architecture_summary.py +11 -3
  21. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_classifier.py +17 -0
  22. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_detectors_base.py +10 -9
  23. sourcecode-0.30.0/tests/test_pipeline_integrity.py +452 -0
  24. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_real_projects.py +4 -2
  25. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_signal_hierarchy.py +5 -3
  26. {sourcecode-0.28.0 → sourcecode-0.30.0}/.gitignore +0 -0
  27. {sourcecode-0.28.0 → sourcecode-0.30.0}/.ruff.toml +0 -0
  28. {sourcecode-0.28.0 → sourcecode-0.30.0}/CONTRIBUTING.md +0 -0
  29. {sourcecode-0.28.0 → sourcecode-0.30.0}/LICENSE +0 -0
  30. {sourcecode-0.28.0 → sourcecode-0.30.0}/README.md +0 -0
  31. {sourcecode-0.28.0 → sourcecode-0.30.0}/SECURITY.md +0 -0
  32. {sourcecode-0.28.0 → sourcecode-0.30.0}/docs/privacy.md +0 -0
  33. {sourcecode-0.28.0 → sourcecode-0.30.0}/docs/schema.md +0 -0
  34. {sourcecode-0.28.0 → sourcecode-0.30.0}/raw +0 -0
  35. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  36. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/context_summarizer.py +0 -0
  37. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/coverage_parser.py +0 -0
  38. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/dependency_analyzer.py +0 -0
  39. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/__init__.py +0 -0
  40. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/base.py +0 -0
  41. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  42. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/dart.py +0 -0
  43. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/dotnet.py +0 -0
  44. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/elixir.py +0 -0
  45. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/go.py +0 -0
  46. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/hybrid.py +0 -0
  47. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/java.py +0 -0
  48. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  49. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/parsers.py +0 -0
  50. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/php.py +0 -0
  51. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/python.py +0 -0
  52. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/ruby.py +0 -0
  53. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/rust.py +0 -0
  54. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/systems.py +0 -0
  55. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/terraform.py +0 -0
  56. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/detectors/tooling.py +0 -0
  57. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/doc_analyzer.py +0 -0
  58. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/graph_analyzer.py +0 -0
  59. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/metrics_analyzer.py +0 -0
  60. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/prepare_context.py +0 -0
  61. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/redactor.py +0 -0
  62. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/relevance_scorer.py +0 -0
  63. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/runtime_classifier.py +0 -0
  64. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/scanner.py +0 -0
  65. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/semantic_analyzer.py +0 -0
  66. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/summarizer.py +0 -0
  67. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/telemetry/__init__.py +0 -0
  68. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/telemetry/config.py +0 -0
  69. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/telemetry/consent.py +0 -0
  70. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/telemetry/events.py +0 -0
  71. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/telemetry/filters.py +0 -0
  72. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/telemetry/transport.py +0 -0
  73. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/tree_utils.py +0 -0
  74. {sourcecode-0.28.0 → sourcecode-0.30.0}/src/sourcecode/workspace.py +0 -0
  75. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/__init__.py +0 -0
  76. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/conftest.py +0 -0
  77. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/coverage.xml +0 -0
  78. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  79. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  80. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  81. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/go_service/go.mod +0 -0
  82. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/jacoco.xml +0 -0
  83. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/lcov.info +0 -0
  84. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  85. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/nextjs_app/package.json +0 -0
  86. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  87. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  88. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  89. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  90. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  91. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  92. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_architecture_analyzer.py +0 -0
  93. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_cli.py +0 -0
  94. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_code_notes_analyzer.py +0 -0
  95. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_coverage_parser.py +0 -0
  96. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_cross_consistency.py +0 -0
  97. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  98. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  99. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_dependency_schema.py +0 -0
  100. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_detector_dotnet.py +0 -0
  101. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_detector_go_rust_java.py +0 -0
  102. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_detector_nodejs.py +0 -0
  103. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_detector_php_ruby_dart.py +0 -0
  104. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_detector_python.py +0 -0
  105. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_detector_universal_managed.py +0 -0
  106. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_detector_universal_systems.py +0 -0
  107. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  108. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_doc_analyzer_python.py +0 -0
  109. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  110. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_graph_analyzer_python_node.py +0 -0
  111. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_graph_schema.py +0 -0
  112. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_hybrid_inference.py +0 -0
  113. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_integration.py +0 -0
  114. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_integration_dependencies.py +0 -0
  115. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_integration_detection.py +0 -0
  116. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_integration_docs.py +0 -0
  117. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_integration_graph_modules.py +0 -0
  118. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_integration_lqn.py +0 -0
  119. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_integration_metrics.py +0 -0
  120. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_integration_multistack.py +0 -0
  121. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_integration_semantics.py +0 -0
  122. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_integration_universal.py +0 -0
  123. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_metrics_analyzer.py +0 -0
  124. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_packaging.py +0 -0
  125. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_phase1_improvements.py +0 -0
  126. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_redactor.py +0 -0
  127. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_scanner.py +0 -0
  128. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_schema.py +0 -0
  129. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_schema_normalization.py +0 -0
  130. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_semantic_analyzer_node.py +0 -0
  131. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_semantic_analyzer_python.py +0 -0
  132. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_semantic_import_resolution.py +0 -0
  133. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_semantic_schema.py +0 -0
  134. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_summarizer.py +0 -0
  135. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_telemetry.py +0 -0
  136. {sourcecode-0.28.0 → sourcecode-0.30.0}/tests/test_workspace_analyzer.py +0 -0
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: "source-command-gsd-join-discord"
3
+ description: "Join the GSD Discord community"
4
+ ---
5
+
6
+ # source-command-gsd-join-discord
7
+
8
+ Use this skill when the user asks to run the migrated source command `gsd-join-discord`.
9
+
10
+ ## Command Template
11
+
12
+ <objective>
13
+ Display the Discord invite link for the GSD community server.
14
+ </objective>
15
+
16
+ <output>
17
+ # Join the GSD Discord
18
+
19
+ Connect with other GSD users, get help, share what you're building, and stay updated.
20
+
21
+ **Invite link:** https://discord.gg/mYgfVNfA2r
22
+
23
+ Click the link or paste it into your browser to join.
24
+ </output>
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: "source-command-gsd-review-backlog"
3
+ description: "Review and promote backlog items to active milestone"
4
+ ---
5
+
6
+ # source-command-gsd-review-backlog
7
+
8
+ Use this skill when the user asks to run the migrated source command `gsd-review-backlog`.
9
+
10
+ ## Command Template
11
+
12
+ <objective>
13
+ Review all 999.x backlog items and optionally promote them into the active
14
+ milestone sequence or remove stale entries.
15
+ </objective>
16
+
17
+ <process>
18
+
19
+ 1. **List backlog items:**
20
+ ```bash
21
+ ls -d .planning/phases/999* 2>/dev/null || echo "No backlog items found"
22
+ ```
23
+
24
+ 2. **Read ROADMAP.md** and extract all 999.x phase entries:
25
+ ```bash
26
+ cat .planning/ROADMAP.md
27
+ ```
28
+ Show each backlog item with its description, any accumulated context (CONTEXT.md, RESEARCH.md), and creation date.
29
+
30
+ 3. **Present the list to the user** via AskUserQuestion:
31
+ - For each backlog item, show: phase number, description, accumulated artifacts
32
+ - Options per item: **Promote** (move to active), **Keep** (leave in backlog), **Remove** (delete)
33
+
34
+ 4. **For items to PROMOTE:**
35
+ - Find the next sequential phase number in the active milestone
36
+ - Rename the directory from `999.x-slug` to `{new_num}-slug`:
37
+ ```bash
38
+ NEW_NUM=$(node "C:/Users/dominique.haroun_m3i/Documents/workspace/test-gsd/.Codex/get-shit-done/bin/gsd-tools.cjs" phase add "${DESCRIPTION}" --raw)
39
+ ```
40
+ - Move accumulated artifacts to the new phase directory
41
+ - Update ROADMAP.md: move the entry from `## Backlog` section to the active phase list
42
+ - Remove `(BACKLOG)` marker
43
+ - Add appropriate `**Depends on:**` field
44
+
45
+ 5. **For items to REMOVE:**
46
+ - Delete the phase directory
47
+ - Remove the entry from ROADMAP.md `## Backlog` section
48
+
49
+ 6. **Commit changes:**
50
+ ```bash
51
+ node "C:/Users/dominique.haroun_m3i/Documents/workspace/test-gsd/.Codex/get-shit-done/bin/gsd-tools.cjs" commit "docs: review backlog — promoted N, removed M" --files .planning/ROADMAP.md
52
+ ```
53
+
54
+ 7. **Report summary:**
55
+ ```
56
+ ## 📋 Backlog Review Complete
57
+
58
+ Promoted: {list of promoted items with new phase numbers}
59
+ Kept: {list of items remaining in backlog}
60
+ Removed: {list of deleted items}
61
+ ```
62
+
63
+ </process>
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: "source-command-gsd-workstreams"
3
+ description: "Manage parallel workstreams — list, create, switch, status, progress, complete, and resume"
4
+ ---
5
+
6
+ # source-command-gsd-workstreams
7
+
8
+ Use this skill when the user asks to run the migrated source command `gsd-workstreams`.
9
+
10
+ ## Command Template
11
+
12
+ # /gsd-workstreams
13
+
14
+ Manage parallel workstreams for concurrent milestone work.
15
+
16
+ ## Usage
17
+
18
+ `/gsd-workstreams [subcommand] [args]`
19
+
20
+ ### Subcommands
21
+
22
+ | Command | Description |
23
+ |---------|-------------|
24
+ | `list` | List all workstreams with status |
25
+ | `create <name>` | Create a new workstream |
26
+ | `status <name>` | Detailed status for one workstream |
27
+ | `switch <name>` | Set active workstream |
28
+ | `progress` | Progress summary across all workstreams |
29
+ | `complete <name>` | Archive a completed workstream |
30
+ | `resume <name>` | Resume work in a workstream |
31
+
32
+ ## Step 1: Parse Subcommand
33
+
34
+ Parse the user's input to determine which workstream operation to perform.
35
+ If no subcommand given, default to `list`.
36
+
37
+ ## Step 2: Execute Operation
38
+
39
+ ### list
40
+ Run: `node "C:/Users/dominique.haroun_m3i/Documents/workspace/test-gsd/.Codex/get-shit-done/bin/gsd-tools.cjs" workstream list --raw --cwd "$CWD"`
41
+ Display the workstreams in a table format showing name, status, current phase, and progress.
42
+
43
+ ### create
44
+ Run: `node "C:/Users/dominique.haroun_m3i/Documents/workspace/test-gsd/.Codex/get-shit-done/bin/gsd-tools.cjs" workstream create <name> --raw --cwd "$CWD"`
45
+ After creation, display the new workstream path and suggest next steps:
46
+ - `/gsd-new-milestone --ws <name>` to set up the milestone
47
+
48
+ ### status
49
+ Run: `node "C:/Users/dominique.haroun_m3i/Documents/workspace/test-gsd/.Codex/get-shit-done/bin/gsd-tools.cjs" workstream status <name> --raw --cwd "$CWD"`
50
+ Display detailed phase breakdown and state information.
51
+
52
+ ### switch
53
+ Run: `node "C:/Users/dominique.haroun_m3i/Documents/workspace/test-gsd/.Codex/get-shit-done/bin/gsd-tools.cjs" workstream set <name> --raw --cwd "$CWD"`
54
+ Also set `GSD_WORKSTREAM` for the current session when the runtime supports it.
55
+ If the runtime exposes a session identifier, GSD also stores the active workstream
56
+ session-locally so concurrent sessions do not overwrite each other.
57
+
58
+ ### progress
59
+ Run: `node "C:/Users/dominique.haroun_m3i/Documents/workspace/test-gsd/.Codex/get-shit-done/bin/gsd-tools.cjs" workstream progress --raw --cwd "$CWD"`
60
+ Display a progress overview across all workstreams.
61
+
62
+ ### complete
63
+ Run: `node "C:/Users/dominique.haroun_m3i/Documents/workspace/test-gsd/.Codex/get-shit-done/bin/gsd-tools.cjs" workstream complete <name> --raw --cwd "$CWD"`
64
+ Archive the workstream to milestones/.
65
+
66
+ ### resume
67
+ Set the workstream as active and suggest `/gsd-resume-work --ws <name>`.
68
+
69
+ ## Step 3: Display Results
70
+
71
+ Format the JSON output from gsd-tools into a human-readable display.
72
+ Include the `${GSD_WS}` flag in any routing suggestions.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 0.28.0
3
+ Version: 0.30.0
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "0.28.0"
7
+ version = "0.30.0"
8
8
  description = "Deterministic codebase context for AI coding agents"
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.28.0"
3
+ __version__ = "0.30.0"
@@ -12,6 +12,10 @@ from sourcecode.schema import (
12
12
  SourceMap,
13
13
  )
14
14
 
15
+ _WORKSPACE_CONFIG_FILES: frozenset[str] = frozenset({
16
+ "turbo.json", "nx.json", "pnpm-workspace.yaml", "lerna.json", "rush.json",
17
+ })
18
+
15
19
  _TOOLING_PREFIXES = (
16
20
  ".claude/",
17
21
  ".vscode/",
@@ -34,6 +38,18 @@ _CODE_EXTENSIONS = {
34
38
  _GENERIC_NAMES = {"utils", "helpers", "common", "shared", "misc", "core", "root", ""}
35
39
 
36
40
  _TEST_DIRS: frozenset[str] = frozenset({"tests", "test", "spec", "specs", "__tests__", "e2e"})
41
+ _BENCHMARK_DIRS: frozenset[str] = frozenset({
42
+ "benchmark", "benchmarks", "bench",
43
+ "example", "examples",
44
+ "demo", "demos",
45
+ "playground", "playgrounds",
46
+ "fixture", "fixtures",
47
+ "sandbox",
48
+ })
49
+ _DOCS_DIRS: frozenset[str] = frozenset({"docs", "doc", "documentation", "wiki"})
50
+ _TOOLING_DIRS: frozenset[str] = frozenset({"scripts", "script", "tools", "tool", "ci"})
51
+ # All dirs that are not part of the runtime source architecture
52
+ _NON_SOURCE_DIRS: frozenset[str] = _TEST_DIRS | _BENCHMARK_DIRS | _DOCS_DIRS | _TOOLING_DIRS
37
53
 
38
54
  # Exact file stems that signal a specific architectural layer
39
55
  _LAYER_STEM_EXACT: dict[str, str] = {
@@ -177,15 +193,35 @@ class ArchitectureAnalyzer:
177
193
  elif pattern == "unknown":
178
194
  limitations.append("Patron de capas no reconocido: estructura de directorios sin senales claras")
179
195
 
196
+ # Step 3b: monorepo override — workspace config is hard evidence
197
+ if self._has_workspace_config(sm.file_paths) and pattern not in (
198
+ "monorepo", "cqrs", "clean", "onion", "hexagonal"
199
+ ):
200
+ mono_layers = self._detect_monorepo_packages(filtered)
201
+ if mono_layers or pattern in (None, "unknown", "flat", "modular", "layered"):
202
+ pattern = "monorepo"
203
+ layers = mono_layers
204
+ limitations.append(
205
+ "Workspace config detectado — arquitectura refleja topologia de paquetes"
206
+ )
207
+
180
208
  # Step 4: bounded context inference
181
209
  bounded_contexts = self._infer_bounded_contexts(domains, graph)
182
210
 
183
- # Overall confidence
211
+ # Overall confidence — based on domain quality, not raw count
184
212
  confidence: Literal["high", "medium", "low"]
213
+ strong_domains = [d for d in domains if d.confidence in ("high", "medium")]
214
+ all_layers_weak = layers and all(l.confidence == "low" for l in layers)
185
215
  if pattern not in (None, "unknown", "flat"):
186
- # A recognised pattern was detected — at least medium confidence
187
- confidence = "high" if len(domains) >= 3 else "medium"
188
- elif len(domains) >= 1:
216
+ if all_layers_weak:
217
+ # Layers came from file-naming heuristic only, not directory structure
218
+ confidence = "medium"
219
+ limitations.append(
220
+ "Patron inferido de nombres de archivo — sin estructura de directorios confirmatoria"
221
+ )
222
+ else:
223
+ confidence = "high" if len(strong_domains) >= 3 else "medium"
224
+ elif len(strong_domains) >= 1:
189
225
  confidence = "medium"
190
226
  else:
191
227
  confidence = "low"
@@ -217,6 +253,10 @@ class ArchitectureAnalyzer:
217
253
  norm = p.replace("\\", "/")
218
254
  if self._is_tooling(norm):
219
255
  continue
256
+ # Exclude non-source dirs at every path segment (benchmarks, docs, tests, scripts…)
257
+ parts = norm.split("/")
258
+ if any(part.lower() in _NON_SOURCE_DIRS for part in parts[:-1]):
259
+ continue
220
260
  ext = Path(norm).suffix.lower()
221
261
  if ext not in _CODE_EXTENSIONS:
222
262
  continue
@@ -250,6 +290,8 @@ class ArchitectureAnalyzer:
250
290
  for name, files in groups.items():
251
291
  if len(files) < 2:
252
292
  continue
293
+ if name.lower() in _NON_SOURCE_DIRS:
294
+ continue
253
295
  role = DOMAIN_ROLES.get(name, "")
254
296
  domain_confidence: Literal["high", "medium", "low"]
255
297
  if name in DOMAIN_ROLES:
@@ -262,10 +304,10 @@ class ArchitectureAnalyzer:
262
304
  return domains
263
305
 
264
306
  def _detect_layers(self, paths: list[str]) -> tuple[str, list[ArchitectureLayer]]:
265
- # Exclude test paths so test directories don't skew layer scoring
307
+ # Exclude non-source paths (tests, benchmarks, docs, tooling) from layer scoring
266
308
  source_paths = [
267
309
  p for p in paths
268
- if not any(part.lower() in _TEST_DIRS for part in p.replace("\\", "/").split("/"))
310
+ if not any(part.lower() in _NON_SOURCE_DIRS for part in p.replace("\\", "/").split("/"))
269
311
  ]
270
312
  if not source_paths:
271
313
  return "unknown", []
@@ -360,7 +402,7 @@ class ArchitectureAnalyzer:
360
402
  parts = p.replace("\\", "/").split("/")
361
403
  if len(parts) >= 2 and parts[-1].lower() in _ENTRY_FILES:
362
404
  top = parts[0]
363
- if top.lower() not in _SRC_TRANSPARENT and top.lower() not in _TEST_DIRS:
405
+ if top.lower() not in _SRC_TRANSPARENT and top.lower() not in _NON_SOURCE_DIRS:
364
406
  entry_dirs.setdefault(top, []).append(p)
365
407
  if len(entry_dirs) >= 4:
366
408
  return "microservices", [
@@ -394,7 +436,7 @@ class ArchitectureAnalyzer:
394
436
  non_empty = {k: v for k, v in layer_files.items() if v}
395
437
  if len(non_empty) >= 2:
396
438
  return "layered", [
397
- ArchitectureLayer(name=k, pattern="layered", files=v, confidence="medium")
439
+ ArchitectureLayer(name=k, pattern="layered", files=v, confidence="low")
398
440
  for k, v in non_empty.items()
399
441
  ]
400
442
  return None
@@ -412,19 +454,42 @@ class ArchitectureAnalyzer:
412
454
  parts = p.replace("\\", "/").split("/")
413
455
  for part in parts[:-1]:
414
456
  if (part not in _SRC_TRANSPARENT
415
- and part.lower() not in _TEST_DIRS
457
+ and part.lower() not in _NON_SOURCE_DIRS
416
458
  and part.lower() not in _GENERIC_NAMES):
417
459
  module_files.setdefault(part, []).append(p)
418
460
  break
419
461
 
420
- meaningful = {k: v for k, v in module_files.items() if len(v) >= 2}
462
+ meaningful = {k: v for k, v in module_files.items() if len(v) >= 3}
421
463
  if len(meaningful) >= 2:
422
464
  return "modular", [
423
- ArchitectureLayer(name=k, pattern="modular", files=v, confidence="medium")
465
+ ArchitectureLayer(name=k, pattern="modular", files=v, confidence="low")
424
466
  for k, v in meaningful.items()
425
467
  ]
426
468
  return None
427
469
 
470
+ def _has_workspace_config(self, file_paths: list[str]) -> bool:
471
+ for path in file_paths:
472
+ parts = path.replace("\\", "/").split("/")
473
+ if len(parts) == 1 and parts[0] in _WORKSPACE_CONFIG_FILES:
474
+ return True
475
+ return False
476
+
477
+ def _detect_monorepo_packages(self, paths: list[str]) -> list[ArchitectureLayer]:
478
+ """Find workspace packages (packages/*, apps/*, libs/*) in a monorepo."""
479
+ _WORKSPACE_ROOTS = {"packages", "apps", "libs", "applications"}
480
+ groups: dict[str, list[str]] = {}
481
+ for p in paths:
482
+ parts = p.replace("\\", "/").split("/")
483
+ if len(parts) >= 2 and parts[0].lower() in _WORKSPACE_ROOTS:
484
+ key = f"{parts[0]}/{parts[1]}"
485
+ groups.setdefault(key, []).append(p)
486
+ result = [
487
+ ArchitectureLayer(name=k, pattern="monorepo", files=v, confidence="medium")
488
+ for k, v in groups.items()
489
+ if len(v) >= 2
490
+ ]
491
+ return result[:16]
492
+
428
493
  def _infer_bounded_contexts(
429
494
  self,
430
495
  domains: list[ArchitectureDomain],
@@ -5,6 +5,7 @@ import re
5
5
  from pathlib import Path
6
6
  from typing import Any
7
7
 
8
+ from sourcecode.entrypoint_classifier import is_production_entry_point
8
9
  from sourcecode.schema import EntryPoint, SourceMap, StackDetection
9
10
  from sourcecode.tree_utils import flatten_file_tree
10
11
 
@@ -63,11 +64,8 @@ class ArchitectureSummarizer:
63
64
  entry for entry in sm.entry_points
64
65
  if not self._is_tooling_path(entry.path)
65
66
  and not self._is_auxiliary_path(entry.path)
66
- and entry.entrypoint_type not in ("benchmark", "example")
67
+ and is_production_entry_point(entry)
67
68
  ]
68
- if not entry_points:
69
- fallback = self._infer_fallback_entry_points(file_paths, sm.stacks)
70
- entry_points = fallback[:1]
71
69
 
72
70
  lang_lines: list[str] = []
73
71
  if entry_points:
@@ -280,8 +278,7 @@ class ArchitectureSummarizer:
280
278
  if modules:
281
279
  formatted = self._format_module_list([self._module_label(module) for module in modules])
282
280
  if formatted:
283
- lines.append(f"Orquesta modulos internos: {formatted}.")
284
- lines.append("Produce la salida principal del entry point JavaScript/TypeScript detectado.")
281
+ lines.append(f"Imports internos del entry point: {formatted}.")
285
282
  return lines
286
283
 
287
284
  def _summarize_java_entry(self, path: str, content: str, stacks: list[StackDetection]) -> list[str]:
@@ -344,8 +341,7 @@ class ArchitectureSummarizer:
344
341
  if internal:
345
342
  formatted = self._format_module_list([self._module_label(module) for module in internal])
346
343
  if formatted:
347
- lines.append(f"Orquesta paquetes internos: {formatted}.")
348
- lines.append("Produce la salida principal del binario Go detectado.")
344
+ lines.append(f"Imports internos del binario Go: {formatted}.")
349
345
  return lines
350
346
 
351
347
  def _describe_entry_point(self, entry_point: EntryPoint, project_type: str | None) -> str:
@@ -45,8 +45,12 @@ class TypeClassifier:
45
45
  primary_stack = self._select_primary_stack(enriched, project_type)
46
46
 
47
47
  final_stacks: list[StackDetection] = []
48
+ primary_assigned = False
48
49
  for stack in enriched:
49
- final_stacks.append(replace(stack, primary=(stack.stack == primary_stack)))
50
+ is_primary = stack.stack == primary_stack and not primary_assigned
51
+ if is_primary:
52
+ primary_assigned = True
53
+ final_stacks.append(replace(stack, primary=is_primary))
50
54
  return final_stacks, project_type
51
55
 
52
56
  def _enrich_stack(
@@ -1,13 +1,141 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
3
4
  import json
4
5
  import time
5
6
  from pathlib import Path
6
7
  from typing import Any, Optional, cast
7
8
 
8
- import typer
9
+ import typer
10
+
11
+ from sourcecode import __version__
12
+ from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
9
13
 
10
- from sourcecode import __version__
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Analyzer fingerprints — short hashes of each analyzer's key rule constants.
17
+ # A change in heuristics, filter lists, or pattern maps changes the hash,
18
+ # making it immediately visible that two runs used different rule versions
19
+ # even if the semver string is the same.
20
+ # ---------------------------------------------------------------------------
21
+
22
+ def _fingerprint(*objects: object) -> str:
23
+ raw = json.dumps([repr(o) for o in objects], sort_keys=True)
24
+ return hashlib.sha256(raw.encode()).hexdigest()[:8]
25
+
26
+
27
+ def _compute_analyzer_fingerprints() -> dict[str, str]:
28
+ from sourcecode.detectors.heuristic import (
29
+ _AUXILIARY_DIRS as _HEUR_AUX,
30
+ _ENTRYPOINT_NAMES,
31
+ _EXTENSION_MAP,
32
+ )
33
+ from sourcecode.detectors.nodejs import _FRAMEWORK_MAP, NodejsDetector
34
+ from sourcecode.confidence_analyzer import (
35
+ _AUXILIARY_DIR_PREFIXES,
36
+ _HARD_SOURCES,
37
+ _SOFT_SOURCES,
38
+ )
39
+ from sourcecode.architecture_analyzer import (
40
+ _BENCHMARK_DIRS,
41
+ _NON_SOURCE_DIRS,
42
+ LAYER_PATTERNS,
43
+ )
44
+
45
+ return {
46
+ "heuristic": _fingerprint(_EXTENSION_MAP, _ENTRYPOINT_NAMES, sorted(_HEUR_AUX)),
47
+ "nodejs": _fingerprint(_FRAMEWORK_MAP, sorted(NodejsDetector._AUXILIARY_DIRS)),
48
+ "confidence": _fingerprint(sorted(_AUXILIARY_DIR_PREFIXES), sorted(_HARD_SOURCES), sorted(_SOFT_SOURCES)),
49
+ "architecture": _fingerprint(sorted(_BENCHMARK_DIRS), sorted(_NON_SOURCE_DIRS), list(LAYER_PATTERNS.keys())),
50
+ }
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Pipeline trace collector
55
+ # ---------------------------------------------------------------------------
56
+
57
+ class _TraceCollector:
58
+ """Lightweight collector for pipeline trace events."""
59
+
60
+ def __init__(self, enabled: bool = False) -> None:
61
+ self._enabled = enabled
62
+ self._events: list[dict[str, Any]] = []
63
+
64
+ def emit(
65
+ self,
66
+ stage: str,
67
+ component: str,
68
+ action: str,
69
+ target: Optional[str] = None,
70
+ reason: Optional[str] = None,
71
+ ) -> None:
72
+ if not self._enabled:
73
+ return
74
+ self._events.append({
75
+ "stage": stage,
76
+ "component": component,
77
+ "action": action,
78
+ **({"target": target} if target else {}),
79
+ **({"reason": reason} if reason else {}),
80
+ })
81
+
82
+ def build_trace(self) -> "PipelineTrace":
83
+ from sourcecode.schema import PipelineEvent, PipelineTrace
84
+ events = [
85
+ PipelineEvent(
86
+ stage=e["stage"],
87
+ component=e["component"],
88
+ action=e["action"],
89
+ target=e.get("target"),
90
+ reason=e.get("reason"),
91
+ )
92
+ for e in self._events
93
+ ]
94
+ return PipelineTrace(requested=True, events=events)
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # E2E pipeline coherence check
99
+ # ---------------------------------------------------------------------------
100
+
101
+ def _check_pipeline_coherence(sm: "SourceMap") -> list[str]: # type: ignore[name-defined]
102
+ """Verify no contradictory states exist between analyzers.
103
+
104
+ Returns a list of human-readable violation strings (empty when clean).
105
+ These are emitted to stderr as [coherence] warnings — never abort a run.
106
+ """
107
+ issues: list[str] = []
108
+ cs = sm.confidence_summary
109
+
110
+ if cs is not None:
111
+ # overall:high requires at least one manifest-detected stack
112
+ if cs.overall == "high":
113
+ manifest_stacks = [s for s in sm.stacks if s.detection_method != "heuristic"]
114
+ if not manifest_stacks:
115
+ issues.append(
116
+ "[coherence] overall=high but all stacks are heuristic — "
117
+ "downgrade not applied; check confidence_analyzer"
118
+ )
119
+
120
+ # overall:high requires at least one production entry point
121
+ if cs.overall == "high":
122
+ prod_eps = [
123
+ ep for ep in sm.entry_points
124
+ if is_production_entry_point(ep)
125
+ ]
126
+ if not prod_eps and sm.entry_points:
127
+ issues.append(
128
+ "[coherence] overall=high but no production entry points exist — "
129
+ "all detected EPs are auxiliary (benchmark/example/dev)"
130
+ )
131
+
132
+ # entry_point_confidence must not be high when entry_points is empty
133
+ if cs.entry_point_confidence == "high" and not sm.entry_points:
134
+ issues.append(
135
+ "[coherence] entry_point_confidence=high but entry_points is empty"
136
+ )
137
+
138
+ return issues
11
139
 
12
140
  _HELP = """\
13
141
  Deterministic codebase context for AI coding agents.
@@ -327,6 +455,11 @@ def main(
327
455
  "--agent",
328
456
  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.",
329
457
  ),
458
+ trace_pipeline: bool = typer.Option(
459
+ False,
460
+ "--trace-pipeline",
461
+ help="Modo trazabilidad: incluye pipeline_trace con candidatos, filtros, descartes y origen de cada dato. Para diagnóstico de contaminación de resultados.",
462
+ ),
330
463
  ) -> None:
331
464
  """Analyze a repository and produce structured context for AI coding agents.
332
465
 
@@ -672,7 +805,18 @@ def main(
672
805
  )
673
806
 
674
807
  # 3. Construir el schema
675
- metadata = AnalysisMetadata(analyzed_path=str(target))
808
+ # Compute analyzer fingerprints: short hashes of each analyzer's key rule
809
+ # constants so that a rule change is always visible in the output, regardless
810
+ # of whether the semver was bumped.
811
+ try:
812
+ _fingerprints = _compute_analyzer_fingerprints()
813
+ except Exception:
814
+ _fingerprints = {}
815
+
816
+ metadata = AnalysisMetadata(
817
+ analyzed_path=str(target),
818
+ analyzer_fingerprints=_fingerprints,
819
+ )
676
820
  sm = SourceMap(
677
821
  metadata=metadata,
678
822
  file_tree=file_tree,
@@ -812,6 +956,52 @@ def main(
812
956
  _conf_summary, _analysis_gaps = ConfidenceAnalyzer().analyze(sm)
813
957
  sm = _replace(sm, confidence_summary=_conf_summary, analysis_gaps=_analysis_gaps)
814
958
 
959
+ # E2E pipeline coherence check — emits [coherence] warnings to stderr.
960
+ # Catches contradictory states that can survive individual-analyzer validation.
961
+ for _issue in _check_pipeline_coherence(sm):
962
+ typer.echo(_issue, err=True)
963
+
964
+ # Build pipeline trace when --trace-pipeline is set.
965
+ if trace_pipeline:
966
+ _trace = _TraceCollector(enabled=True)
967
+ _trace.emit("scan", "scanner", "complete",
968
+ reason=f"{len(sm.file_paths)} files, {len(manifests)} manifests")
969
+ for _s in sm.stacks:
970
+ _trace.emit("detect", _s.produced_by or "unknown", "emit_stack",
971
+ target=_s.stack,
972
+ reason=f"method={_s.detection_method} confidence={_s.confidence}")
973
+ for _ep in sm.entry_points:
974
+ _trace.emit("detect", _ep.produced_by or "unknown", "emit_ep",
975
+ target=_ep.path,
976
+ reason=f"type={_ep.entrypoint_type} confidence={_ep.confidence} reason={_ep.reason}")
977
+ # Record EPs filtered from agent_view (benchmark/example with path-auxiliary parts)
978
+ _aux_parts = frozenset({
979
+ "benchmark", "benchmarks", "bench", "demo", "demos",
980
+ "example", "examples", "docs", "doc", "fixtures", "fixture",
981
+ })
982
+ for _ep in sm.entry_points:
983
+ _normalized_ep = normalize_entry_point(_ep)
984
+ _ep_type = _normalized_ep.entrypoint_type
985
+ _path_parts = _ep.path.replace("\\", "/").lower().split("/")
986
+ _filtered = (
987
+ _normalized_ep.classification != "production"
988
+ or any(p in _aux_parts for p in _path_parts)
989
+ )
990
+ if _filtered:
991
+ _trace.emit("output", "agent_view", "filter_ep",
992
+ target=_ep.path,
993
+ reason=f"entrypoint_type={_ep_type} (auxiliary)")
994
+ if sm.confidence_summary is not None:
995
+ _cs = sm.confidence_summary
996
+ _trace.emit("confidence", "confidence_analyzer", "computed",
997
+ reason=(
998
+ f"overall={_cs.overall} "
999
+ f"stack={_cs.stack_confidence} "
1000
+ f"ep={_cs.entry_point_confidence} "
1001
+ f"anomalies={len(_cs.anomalies)}"
1002
+ ))
1003
+ sm = _replace(sm, pipeline_trace=_trace.build_trace())
1004
+
815
1005
  # 4. Serializar
816
1006
  if agent:
817
1007
  data = agent_view(sm)