sourcecode 1.52.0__tar.gz → 1.53.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 (121) hide show
  1. {sourcecode-1.52.0 → sourcecode-1.53.0}/PKG-INFO +25 -4
  2. {sourcecode-1.52.0 → sourcecode-1.53.0}/README.md +24 -3
  3. {sourcecode-1.52.0 → sourcecode-1.53.0}/pyproject.toml +1 -1
  4. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/cli.py +390 -1
  6. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/format_contract.py +1 -0
  7. sourcecode-1.53.0/src/sourcecode/integration_detector.py +163 -0
  8. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/repository_ir.py +45 -6
  9. {sourcecode-1.52.0 → sourcecode-1.53.0}/.github/workflows/build-windows.yml +0 -0
  10. {sourcecode-1.52.0 → sourcecode-1.53.0}/.gitignore +0 -0
  11. {sourcecode-1.52.0 → sourcecode-1.53.0}/.ruff.toml +0 -0
  12. {sourcecode-1.52.0 → sourcecode-1.53.0}/CHANGELOG.md +0 -0
  13. {sourcecode-1.52.0 → sourcecode-1.53.0}/CONTRIBUTING.md +0 -0
  14. {sourcecode-1.52.0 → sourcecode-1.53.0}/LICENSE +0 -0
  15. {sourcecode-1.52.0 → sourcecode-1.53.0}/SECURITY.md +0 -0
  16. {sourcecode-1.52.0 → sourcecode-1.53.0}/raw +0 -0
  17. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/adaptive_scanner.py +0 -0
  18. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/architecture_analyzer.py +0 -0
  19. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/architecture_summary.py +0 -0
  20. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/ast_extractor.py +0 -0
  21. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/cache.py +0 -0
  22. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/canonical_ir.py +0 -0
  23. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/cir_graphs.py +0 -0
  24. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/classifier.py +0 -0
  25. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  26. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/confidence_analyzer.py +0 -0
  27. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/context_scorer.py +0 -0
  28. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/context_summarizer.py +0 -0
  29. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/contract_model.py +0 -0
  30. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/contract_pipeline.py +0 -0
  31. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/coverage_parser.py +0 -0
  32. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/dependency_analyzer.py +0 -0
  33. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/__init__.py +0 -0
  34. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/base.py +0 -0
  35. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  36. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/dart.py +0 -0
  37. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/dotnet.py +0 -0
  38. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/elixir.py +0 -0
  39. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/go.py +0 -0
  40. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/heuristic.py +0 -0
  41. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/hybrid.py +0 -0
  42. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/java.py +0 -0
  43. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  44. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/nodejs.py +0 -0
  45. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/parsers.py +0 -0
  46. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/php.py +0 -0
  47. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/project.py +0 -0
  48. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/python.py +0 -0
  49. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/ruby.py +0 -0
  50. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/rust.py +0 -0
  51. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/systems.py +0 -0
  52. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/terraform.py +0 -0
  53. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/detectors/tooling.py +0 -0
  54. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/doc_analyzer.py +0 -0
  55. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  56. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/env_analyzer.py +0 -0
  57. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/error_schema.py +0 -0
  58. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/explain.py +0 -0
  59. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/file_chunker.py +0 -0
  60. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/file_classifier.py +0 -0
  61. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/flow_analyzer.py +0 -0
  62. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/fqn_utils.py +0 -0
  63. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/git_analyzer.py +0 -0
  64. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/graph_analyzer.py +0 -0
  65. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/license.py +0 -0
  66. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/mcp/__init__.py +0 -0
  67. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  68. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  69. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  70. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  71. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  72. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  73. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/mcp/registry.py +0 -0
  74. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/mcp/runner.py +0 -0
  75. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/mcp/server.py +0 -0
  76. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/mcp_nudge.py +0 -0
  77. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/metrics_analyzer.py +0 -0
  78. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/migrate_check.py +0 -0
  79. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/openapi_surface.py +0 -0
  80. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/output_budget.py +0 -0
  81. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/path_filters.py +0 -0
  82. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  83. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/pr_impact.py +0 -0
  84. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/prepare_context.py +0 -0
  85. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/progress.py +0 -0
  86. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/ranking_engine.py +0 -0
  87. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/redactor.py +0 -0
  88. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/relevance_scorer.py +0 -0
  89. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/rename_refactor.py +0 -0
  90. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/repo_classifier.py +0 -0
  91. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/ris.py +0 -0
  92. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/runtime_classifier.py +0 -0
  93. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/scanner.py +0 -0
  94. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/schema.py +0 -0
  95. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/security_config.py +0 -0
  96. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/semantic_analyzer.py +0 -0
  97. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/serializer.py +0 -0
  98. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/spring_event_topology.py +0 -0
  99. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/spring_findings.py +0 -0
  100. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/spring_impact.py +0 -0
  101. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/spring_model.py +0 -0
  102. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/spring_security_audit.py +0 -0
  103. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/spring_semantic.py +0 -0
  104. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  105. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/summarizer.py +0 -0
  106. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/telemetry/__init__.py +0 -0
  107. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/telemetry/config.py +0 -0
  108. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/telemetry/consent.py +0 -0
  109. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/telemetry/events.py +0 -0
  110. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/telemetry/filters.py +0 -0
  111. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/telemetry/transport.py +0 -0
  112. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/tree_utils.py +0 -0
  113. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/validation_surface.py +0 -0
  114. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/version_check.py +0 -0
  115. {sourcecode-1.52.0 → sourcecode-1.53.0}/src/sourcecode/workspace.py +0 -0
  116. {sourcecode-1.52.0 → sourcecode-1.53.0}/supabase/functions/README.md +0 -0
  117. {sourcecode-1.52.0 → sourcecode-1.53.0}/supabase/functions/get-license/index.ts +0 -0
  118. {sourcecode-1.52.0 → sourcecode-1.53.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  119. {sourcecode-1.52.0 → sourcecode-1.53.0}/supabase/functions/telemetry/index.ts +0 -0
  120. {sourcecode-1.52.0 → sourcecode-1.53.0}/supabase/sql/license_event_ordering.sql +0 -0
  121. {sourcecode-1.52.0 → sourcecode-1.53.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.52.0
3
+ Version: 1.53.0
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
42
42
 
43
- ![Version](https://img.shields.io/badge/version-1.52.0-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.53.0-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
45
45
 
46
46
  ---
@@ -114,7 +114,7 @@ pipx install sourcecode
114
114
 
115
115
  ```bash
116
116
  sourcecode version
117
- # sourcecode 1.52.0
117
+ # sourcecode 1.53.0
118
118
  ```
119
119
 
120
120
  ---
@@ -364,9 +364,10 @@ sourcecode impact OrderService . --depth 2 # limit BFS depth
364
364
  ```bash
365
365
  sourcecode endpoints /path/to/repo
366
366
  sourcecode endpoints /path/to/repo --output endpoints.json
367
+ sourcecode endpoints /path/to/repo --by-controller
367
368
  ```
368
369
 
369
- Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.) and JAX-RS (`@GET`, `@POST`, `@Path`) endpoint methods. Returns HTTP method, path, controller class, and handler method.
370
+ Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.) and JAX-RS (`@GET`, `@POST`, `@Path`) endpoint methods. Returns HTTP method, path, controller class, and handler method. Each endpoint also carries its `return_type`. `--by-controller` groups the surface per controller (`{by_controller, controller_count, total}`) for an API-surface view.
370
371
 
371
372
  **Functional / WebFlux routing (honest limitation).** Routes registered via the functional DSL — `route().GET("/path", handler)` / `RouterFunction` / `CustomEndpoint`, common in reactive Spring apps — are **not** modeled (their real paths depend on `nest()`/group-version prefixes that can't be resolved statically). Rather than emit partial paths that would mislead, the output reports a `functional_routing` block (`files`, `route_registrations`, `modeled: false`) plus a warning. When the annotation surface is empty but functional routes exist, the warning explicitly tells you not to read it as "no endpoints". Annotation-based (MVC/JAX-RS) repos are unaffected.
372
373
 
@@ -387,6 +388,26 @@ Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.)
387
388
 
388
389
  Matching endpoints then report `policy: "custom"` with `annotation`, `resourceName`, and `requiredLevel`, and are no longer counted in `no_security_signal`. Repos without the config behave exactly as before.
389
390
 
391
+ ### `export` — architecture views for downstream tooling
392
+
393
+ ```bash
394
+ sourcecode export /path/to/repo --by-directory # code map, path:line refs
395
+ sourcecode export /path/to/repo --module-graph # module→module dependencies
396
+ sourcecode export /path/to/repo --integrations # outbound HTTP/LDAP/JMS clients
397
+ sourcecode export /path/to/repo --c4 # unified architecture + manifest
398
+ ```
399
+
400
+ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the kind of input an architecture-doc generator, diagram renderer, or code-search agent can consume directly instead of walking the tree file by file. Section labels map to the open [C4 model](https://c4model.com) (an open architecture notation, not a product); the schema is vendor-neutral.
401
+
402
+ | Flag | Output |
403
+ |------|--------|
404
+ | `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
405
+ | `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
406
+ | `--integrations` | Outbound integrations (`RestTemplate`, `WebClient`, `@FeignClient`, `LdapTemplate`, `JmsTemplate`, ActiveMQ) with `file:line` evidence and a literal `target` URL/name when present. |
407
+ | `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). |
408
+
409
+ The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
410
+
390
411
  ### `spring-audit` — Spring semantic audit [free]
391
412
 
392
413
  ```bash
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.52.0-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.53.0-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
7
7
 
8
8
  ---
@@ -76,7 +76,7 @@ pipx install sourcecode
76
76
 
77
77
  ```bash
78
78
  sourcecode version
79
- # sourcecode 1.52.0
79
+ # sourcecode 1.53.0
80
80
  ```
81
81
 
82
82
  ---
@@ -326,9 +326,10 @@ sourcecode impact OrderService . --depth 2 # limit BFS depth
326
326
  ```bash
327
327
  sourcecode endpoints /path/to/repo
328
328
  sourcecode endpoints /path/to/repo --output endpoints.json
329
+ sourcecode endpoints /path/to/repo --by-controller
329
330
  ```
330
331
 
331
- Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.) and JAX-RS (`@GET`, `@POST`, `@Path`) endpoint methods. Returns HTTP method, path, controller class, and handler method.
332
+ Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.) and JAX-RS (`@GET`, `@POST`, `@Path`) endpoint methods. Returns HTTP method, path, controller class, and handler method. Each endpoint also carries its `return_type`. `--by-controller` groups the surface per controller (`{by_controller, controller_count, total}`) for an API-surface view.
332
333
 
333
334
  **Functional / WebFlux routing (honest limitation).** Routes registered via the functional DSL — `route().GET("/path", handler)` / `RouterFunction` / `CustomEndpoint`, common in reactive Spring apps — are **not** modeled (their real paths depend on `nest()`/group-version prefixes that can't be resolved statically). Rather than emit partial paths that would mislead, the output reports a `functional_routing` block (`files`, `route_registrations`, `modeled: false`) plus a warning. When the annotation surface is empty but functional routes exist, the warning explicitly tells you not to read it as "no endpoints". Annotation-based (MVC/JAX-RS) repos are unaffected.
334
335
 
@@ -349,6 +350,26 @@ Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.)
349
350
 
350
351
  Matching endpoints then report `policy: "custom"` with `annotation`, `resourceName`, and `requiredLevel`, and are no longer counted in `no_security_signal`. Repos without the config behave exactly as before.
351
352
 
353
+ ### `export` — architecture views for downstream tooling
354
+
355
+ ```bash
356
+ sourcecode export /path/to/repo --by-directory # code map, path:line refs
357
+ sourcecode export /path/to/repo --module-graph # module→module dependencies
358
+ sourcecode export /path/to/repo --integrations # outbound HTTP/LDAP/JMS clients
359
+ sourcecode export /path/to/repo --c4 # unified architecture + manifest
360
+ ```
361
+
362
+ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the kind of input an architecture-doc generator, diagram renderer, or code-search agent can consume directly instead of walking the tree file by file. Section labels map to the open [C4 model](https://c4model.com) (an open architecture notation, not a product); the schema is vendor-neutral.
363
+
364
+ | Flag | Output |
365
+ |------|--------|
366
+ | `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
367
+ | `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
368
+ | `--integrations` | Outbound integrations (`RestTemplate`, `WebClient`, `@FeignClient`, `LdapTemplate`, `JmsTemplate`, ActiveMQ) with `file:line` evidence and a literal `target` URL/name when present. |
369
+ | `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). |
370
+
371
+ The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
372
+
352
373
  ### `spring-audit` — Spring semantic audit [free]
353
374
 
354
375
  ```bash
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.52.0"
7
+ version = "1.53.0"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.52.0"
3
+ __version__ = "1.53.0"
@@ -219,7 +219,7 @@ _HELP = _build_help_text()
219
219
  _SUBCOMMANDS: frozenset[str] = frozenset(
220
220
  {
221
221
  "telemetry", "prepare-context", "version", "config",
222
- "repo-ir", "mcp", "endpoints", "impact",
222
+ "repo-ir", "mcp", "endpoints", "impact", "export",
223
223
  # Enterprise workflow commands
224
224
  "onboard", "modernize", "fix-bug", "review-pr",
225
225
  # License / auth
@@ -3804,6 +3804,30 @@ def impact_cmd(
3804
3804
  # canonical single-source-of-truth endpoint extractor.
3805
3805
 
3806
3806
 
3807
+ def _group_endpoints_by_controller(endpoints: "list[dict]") -> "dict":
3808
+ """Group endpoints by their controller FQN into a structured API surface.
3809
+
3810
+ Returns ``{"by_controller": {fqn: [{method, path, return_type}, ...]},
3811
+ "controller_count": int, "total": int}``. Controllers and their routes are
3812
+ ordered deterministically (controllers by name, routes by path then method).
3813
+ """
3814
+ by_ctrl: "dict[str, list[dict]]" = {}
3815
+ for ep in endpoints:
3816
+ ctrl = ep.get("controller", "") or "<unknown>"
3817
+ by_ctrl.setdefault(ctrl, []).append({
3818
+ "method": ep.get("method", ""),
3819
+ "path": ep.get("path", ""),
3820
+ "return_type": ep.get("return_type", "void"),
3821
+ })
3822
+ for ctrl in by_ctrl:
3823
+ by_ctrl[ctrl].sort(key=lambda r: (r["path"], r["method"]))
3824
+ ordered = {k: by_ctrl[k] for k in sorted(by_ctrl)}
3825
+ return {
3826
+ "by_controller": ordered,
3827
+ "controller_count": len(ordered),
3828
+ "total": len(endpoints),
3829
+ }
3830
+
3807
3831
 
3808
3832
  @app.command("endpoints")
3809
3833
  def endpoints_cmd(
@@ -3844,6 +3868,10 @@ def endpoints_cmd(
3844
3868
  False, "--no-cache",
3845
3869
  help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
3846
3870
  ),
3871
+ by_controller: bool = typer.Option(
3872
+ False, "--by-controller",
3873
+ help="Group endpoints by controller class (structured API surface for C4/Container synthesis).",
3874
+ ),
3847
3875
  ) -> None:
3848
3876
  """Extract REST API endpoint surface from Java source files.
3849
3877
 
@@ -3929,6 +3957,11 @@ def endpoints_cmd(
3929
3957
  "undocumented_before_filter": _undoc_before,
3930
3958
  }
3931
3959
 
3960
+ if by_controller:
3961
+ _grouped = _group_endpoints_by_controller(data.get("endpoints", []))
3962
+ data["by_controller"] = _grouped["by_controller"]
3963
+ data["controller_count"] = _grouped["controller_count"]
3964
+
3932
3965
  output = _serialize_dict(data, format)
3933
3966
 
3934
3967
  _emit_command_output(output, output_path, copy,
@@ -3938,6 +3971,362 @@ def endpoints_cmd(
3938
3971
  _nudge()
3939
3972
 
3940
3973
 
3974
+ # ── export ──────────────────────────────────────────────────────────────────
3975
+
3976
+ def _group_symbols_by_directory(nodes: "list[dict]") -> "dict":
3977
+ """Group repo-ir graph nodes by source directory with path:line refs.
3978
+
3979
+ Returns ``{dir: [{symbol, kind, ref}]}`` ordered deterministically (dirs by
3980
+ name, symbols by ref). ``ref`` is ``source_file:line`` when the line is known,
3981
+ otherwise just ``source_file``. This is the per-directory, file:line-anchored
3982
+ code-level export — the file:line-anchored input an architecture/code-map
3983
+ consumer needs to proceed by file write instead of per-directory LLM reads.
3984
+ """
3985
+ import posixpath
3986
+ by_dir: "dict[str, list[dict]]" = {}
3987
+ for n in nodes:
3988
+ sf = n.get("source_file") or ""
3989
+ if not sf:
3990
+ continue
3991
+ d = posixpath.dirname(sf) or "."
3992
+ ln = n.get("line")
3993
+ ref = f"{sf}:{ln}" if ln else sf
3994
+ by_dir.setdefault(d, []).append({
3995
+ "symbol": n.get("canonical_name") or n.get("fqn"),
3996
+ "kind": n.get("symbol_kind"),
3997
+ "ref": ref,
3998
+ })
3999
+ for d in by_dir:
4000
+ by_dir[d].sort(key=lambda r: r["ref"])
4001
+ return {k: by_dir[k] for k in sorted(by_dir)}
4002
+
4003
+
4004
+ def _build_module_graph(nodes: "list[dict]", edges: "list[dict]") -> "dict":
4005
+ """Roll class-level relation edges up into a module→module dependency graph.
4006
+
4007
+ A *module* is the source directory of a symbol (same grouping key as
4008
+ ``--by-directory``), giving C4 a container/component-level dependency view.
4009
+ Every node FQN maps to its module; each edge whose endpoints resolve to two
4010
+ *different* modules contributes one inter-module dependency, aggregated by
4011
+ ``(from, to)`` with a hit count and the set of underlying edge types.
4012
+
4013
+ Edges whose endpoints are not internal nodes (e.g. imports of external
4014
+ library types) are skipped — only resolvable, internal module→module
4015
+ dependencies are reported. Returns ``{nodes, edges, summary}`` deterministically.
4016
+ """
4017
+ import posixpath
4018
+
4019
+ fqn_to_module: "dict[str, str]" = {}
4020
+ module_symbols: "dict[str, int]" = {}
4021
+ for n in nodes:
4022
+ sf = n.get("source_file") or ""
4023
+ if not sf:
4024
+ continue
4025
+ mod = posixpath.dirname(sf) or "."
4026
+ fqn = n.get("fqn")
4027
+ if fqn:
4028
+ fqn_to_module[fqn] = mod
4029
+ module_symbols[mod] = module_symbols.get(mod, 0) + 1
4030
+
4031
+ # (from_mod, to_mod) -> {"count": int, "types": set[str]}
4032
+ agg: "dict[tuple[str, str], dict]" = {}
4033
+ for e in edges:
4034
+ fm = fqn_to_module.get(e.get("from"))
4035
+ tm = fqn_to_module.get(e.get("to"))
4036
+ if fm is None or tm is None or fm == tm:
4037
+ continue
4038
+ slot = agg.setdefault((fm, tm), {"count": 0, "types": set()})
4039
+ slot["count"] += 1
4040
+ et = e.get("type")
4041
+ if et:
4042
+ slot["types"].add(et)
4043
+
4044
+ graph_edges = [
4045
+ {
4046
+ "from": fm,
4047
+ "to": tm,
4048
+ "count": slot["count"],
4049
+ "types": sorted(slot["types"]),
4050
+ }
4051
+ for (fm, tm), slot in sorted(agg.items())
4052
+ ]
4053
+ out_deg: "dict[str, int]" = {}
4054
+ in_deg: "dict[str, int]" = {}
4055
+ for ed in graph_edges:
4056
+ out_deg[ed["from"]] = out_deg.get(ed["from"], 0) + 1
4057
+ in_deg[ed["to"]] = in_deg.get(ed["to"], 0) + 1
4058
+ graph_nodes = [
4059
+ {
4060
+ "module": mod,
4061
+ "symbol_count": module_symbols[mod],
4062
+ "out_degree": out_deg.get(mod, 0),
4063
+ "in_degree": in_deg.get(mod, 0),
4064
+ }
4065
+ for mod in sorted(module_symbols)
4066
+ ]
4067
+ return {
4068
+ "nodes": graph_nodes,
4069
+ "edges": graph_edges,
4070
+ "summary": {
4071
+ "module_count": len(graph_nodes),
4072
+ "edge_count": len(graph_edges),
4073
+ },
4074
+ }
4075
+
4076
+
4077
+ # Build-system markers that identify a deployable/buildable unit (C4 container).
4078
+ _BUILD_MARKERS: "tuple[str, ...]" = (
4079
+ "pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle",
4080
+ )
4081
+
4082
+
4083
+ def _detect_containers(root: "Path") -> "list[dict]":
4084
+ """Detect build-module roots as C4 containers (deployable/buildable units).
4085
+
4086
+ A *container* is a directory holding a recognized build file
4087
+ (Maven/Gradle). Detection is purely structural — no build is run. Returns
4088
+ ``[{root, build_file}]`` (relative paths, deterministic order). Empty when no
4089
+ build files are found; the caller records that as a limitation rather than
4090
+ fabricating containers.
4091
+ """
4092
+ import os
4093
+ found: "list[dict]" = []
4094
+ seen: "set[str]" = set()
4095
+ _SKIP = {".git", "node_modules", "target", "build", ".gradle", ".idea", "dist"}
4096
+ for dirpath, dirnames, filenames in os.walk(root):
4097
+ dirnames[:] = [d for d in dirnames if d not in _SKIP and not d.startswith(".")]
4098
+ for marker in _BUILD_MARKERS:
4099
+ if marker in filenames:
4100
+ rel = os.path.relpath(dirpath, root).replace(os.sep, "/")
4101
+ rel = "." if rel == "." else rel
4102
+ if rel in seen:
4103
+ continue
4104
+ seen.add(rel)
4105
+ found.append({"root": rel, "build_file": marker})
4106
+ break
4107
+ found.sort(key=lambda c: c["root"])
4108
+ return found
4109
+
4110
+
4111
+ def _directory_hashes(file_list: "list[str]", root: "Path") -> "dict[str, str]":
4112
+ """Content-addressed sha256 per source directory for incremental consumers.
4113
+
4114
+ Hash inputs are the directory's source files as ``(relpath, bytes)`` in
4115
+ sorted order, so the digest is stable across runs and changes iff a file in
4116
+ that directory changes. A consumer compares hashes to skip unchanged
4117
+ directories. Tool-agnostic: just a map ``{dir: sha256[:16]}``.
4118
+ """
4119
+ import hashlib
4120
+ import posixpath
4121
+ by_dir: "dict[str, list[str]]" = {}
4122
+ for rel in file_list:
4123
+ d = posixpath.dirname(rel) or "."
4124
+ by_dir.setdefault(d, []).append(rel)
4125
+ out: "dict[str, str]" = {}
4126
+ for d in sorted(by_dir):
4127
+ h = hashlib.sha256()
4128
+ for rel in sorted(by_dir[d]):
4129
+ h.update(rel.encode("utf-8"))
4130
+ h.update(b"\0")
4131
+ try:
4132
+ h.update((root / rel).read_bytes())
4133
+ except OSError:
4134
+ h.update(b"<unreadable>")
4135
+ h.update(b"\0")
4136
+ out[d] = h.hexdigest()[:16]
4137
+ return out
4138
+
4139
+
4140
+ def _build_c4_export(
4141
+ root: "Path",
4142
+ file_list: "list[str]",
4143
+ nodes: "list[dict]",
4144
+ edges: "list[dict]",
4145
+ endpoints: "list[dict]",
4146
+ integrations: "dict",
4147
+ ) -> "dict":
4148
+ """Assemble a unified, tool-agnostic C4 architecture export + incremental manifest.
4149
+
4150
+ Maps the four already-built views onto the open C4 model (a public notation,
4151
+ not a product): code (L4), components (L3/L2), context external systems (L1),
4152
+ plus build-module containers and an interface-contract API surface. The
4153
+ ``manifest`` carries per-directory content hashes so a downstream consumer can
4154
+ process incrementally. No third-party tool or format is hardcoded.
4155
+ """
4156
+ by_directory = _group_symbols_by_directory(nodes)
4157
+ module_graph = _build_module_graph(nodes, edges)
4158
+ api_surface = _group_endpoints_by_controller(endpoints)
4159
+ containers = _detect_containers(root)
4160
+
4161
+ limitations: "list[str]" = []
4162
+ if not containers:
4163
+ limitations.append(
4164
+ "No build files (Maven/Gradle) found; containers not derived. "
4165
+ "Treat the repository as a single implicit container."
4166
+ )
4167
+
4168
+ return {
4169
+ "schema_version": "c4-v1",
4170
+ "c4": {
4171
+ "context": {
4172
+ "system": {"name": root.name, "file_count": len(file_list)},
4173
+ "external_systems": integrations,
4174
+ },
4175
+ "containers": containers,
4176
+ "components": module_graph,
4177
+ "code": by_directory,
4178
+ },
4179
+ "api_surface": api_surface,
4180
+ "manifest": {
4181
+ "directory_hashes": _directory_hashes(file_list, root),
4182
+ "generated": {
4183
+ "tool": "sourcecode",
4184
+ "schema": "c4-v1",
4185
+ "file_count": len(file_list),
4186
+ },
4187
+ },
4188
+ "limitations": limitations,
4189
+ }
4190
+
4191
+
4192
+ @app.command("export")
4193
+ def export_cmd(
4194
+ path: Path = typer.Argument(
4195
+ Path("."),
4196
+ help="Repository path to export (default: current directory)",
4197
+ ),
4198
+ output_path: Optional[Path] = typer.Option(
4199
+ None, "--output", "-o",
4200
+ help="Write output to a file instead of stdout.",
4201
+ ),
4202
+ format: str = typer.Option(
4203
+ "json", "--format", "-f",
4204
+ help="Output format: json (default) or yaml.",
4205
+ ),
4206
+ copy: bool = typer.Option(
4207
+ False, "--copy", "-c",
4208
+ help="Copy output to system clipboard after a successful run.",
4209
+ ),
4210
+ by_directory: bool = typer.Option(
4211
+ False, "--by-directory",
4212
+ help="Group symbols by source directory with path:line refs (C4 code-level export).",
4213
+ ),
4214
+ module_graph: bool = typer.Option(
4215
+ False, "--module-graph",
4216
+ help="Emit a module→module dependency graph (C4 container/component level).",
4217
+ ),
4218
+ integrations: bool = typer.Option(
4219
+ False, "--integrations",
4220
+ help="Detect outbound integrations (HTTP/LDAP/JMS clients) with file:line evidence.",
4221
+ ),
4222
+ c4: bool = typer.Option(
4223
+ False, "--c4",
4224
+ help="Unified C4 architecture export (context/containers/components/code) "
4225
+ "+ per-directory incremental manifest. Vendor-neutral.",
4226
+ ),
4227
+ ) -> None:
4228
+ """Export structured, tool-agnostic codebase views for downstream tooling.
4229
+
4230
+ Output is plain JSON/YAML that any consumer (architecture-doc generators,
4231
+ diagram renderers, code-search agents) can ingest. Section labels map to the
4232
+ open C4 model (an open architecture notation, not a product) but the schema
4233
+ is vendor-neutral.
4234
+
4235
+ \b
4236
+ --by-directory One group per source directory, each symbol carrying a
4237
+ path:line reference — the file:line-anchored code map that
4238
+ lets a consumer proceed by file write instead of per-dir reads.
4239
+ --module-graph Module→module dependency graph (container/component level)
4240
+ rolled up from class-level relation edges.
4241
+ --integrations Outbound integrations (RestTemplate/WebClient/Feign/LDAP/JMS)
4242
+ with file:line evidence — external-system dependency arrows.
4243
+ --c4 Unified architecture document mapped onto the open C4 model
4244
+ (context/containers/components/code) + an API surface and a
4245
+ per-directory content-hash manifest for incremental consumers.
4246
+
4247
+ The section flags compose; pass several to emit multiple sections in one
4248
+ document. --c4 assembles the full architecture export on its own.
4249
+ """
4250
+ from sourcecode.repository_ir import build_repo_ir, find_java_files
4251
+
4252
+ _enforce_format("export", format)
4253
+
4254
+ root = path.resolve()
4255
+ if not root.is_dir():
4256
+ _emit_error_json(
4257
+ INVALID_INPUT_CODE,
4258
+ f"'{root}' is not a valid directory.",
4259
+ path=str(root),
4260
+ hint="Pass an existing repository directory.",
4261
+ expected="A directory path.",
4262
+ )
4263
+ raise typer.Exit(1)
4264
+
4265
+ if not (by_directory or module_graph or integrations or c4):
4266
+ _emit_error_json(
4267
+ INVALID_INPUT_CODE,
4268
+ "export requires a mode flag.",
4269
+ path=str(root),
4270
+ hint="Pass --c4 for the full architecture export, or one of "
4271
+ "--by-directory / --module-graph / --integrations for a section.",
4272
+ expected="--c4 | --by-directory | --module-graph | --integrations",
4273
+ )
4274
+ raise typer.Exit(1)
4275
+
4276
+ file_list = [
4277
+ f for f in find_java_files(root)
4278
+ if "/test/" not in f and "/tests/" not in f
4279
+ ]
4280
+
4281
+ if c4:
4282
+ # Unified architecture export: assembles every section + manifest.
4283
+ from sourcecode.integration_detector import detect_integrations
4284
+ from sourcecode.repository_ir import extract_java_endpoints
4285
+ ir = build_repo_ir(file_list, root)
4286
+ graph = ir.get("graph", {})
4287
+ endpoints = extract_java_endpoints(root).get("endpoints", [])
4288
+ data = _build_c4_export(
4289
+ root,
4290
+ file_list,
4291
+ graph.get("nodes", []),
4292
+ graph.get("edges", []),
4293
+ endpoints,
4294
+ detect_integrations(file_list, root),
4295
+ )
4296
+ output = _serialize_dict(data, format)
4297
+ _emit_command_output(output, output_path, copy,
4298
+ success_msg=f"C4 architecture export written to {output_path}")
4299
+ from sourcecode.mcp_nudge import nudge_mcp_if_needed as _nudge
4300
+ _nudge()
4301
+ return
4302
+
4303
+ data: "dict" = {}
4304
+ # IR is only needed for the symbol/graph-derived views, not for the
4305
+ # source-scanned integration detector.
4306
+ if by_directory or module_graph:
4307
+ ir = build_repo_ir(file_list, root)
4308
+ graph = ir.get("graph", {})
4309
+ nodes = graph.get("nodes", [])
4310
+ if by_directory:
4311
+ grouped = _group_symbols_by_directory(nodes)
4312
+ data["by_directory"] = grouped
4313
+ data["directory_count"] = len(grouped)
4314
+ data["symbol_count"] = sum(len(v) for v in grouped.values())
4315
+ if module_graph:
4316
+ data["module_graph"] = _build_module_graph(nodes, graph.get("edges", []))
4317
+
4318
+ if integrations:
4319
+ from sourcecode.integration_detector import detect_integrations
4320
+ data["integrations"] = detect_integrations(file_list, root)
4321
+
4322
+ output = _serialize_dict(data, format)
4323
+ _emit_command_output(output, output_path, copy,
4324
+ success_msg=f"Export written to {output_path}")
4325
+
4326
+ from sourcecode.mcp_nudge import nudge_mcp_if_needed as _nudge
4327
+ _nudge()
4328
+
4329
+
3941
4330
  @app.command("validation")
3942
4331
  def validation_cmd(
3943
4332
  path: Path = typer.Argument(
@@ -27,6 +27,7 @@ FORMAT_REGISTRY: "dict[str, tuple[str, ...]]" = {
27
27
  "repo-ir": ("json", "yaml"),
28
28
  "impact": ("json", "yaml"),
29
29
  "endpoints": ("json", "yaml"),
30
+ "export": ("json", "yaml"),
30
31
  "validation": ("json", "yaml"),
31
32
  "impact-chain": ("json", "yaml"),
32
33
  "pr-impact": ("text", "json"),