sourcecode 2.5.3__tar.gz → 2.5.5__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 (160) hide show
  1. {sourcecode-2.5.3 → sourcecode-2.5.5}/CHANGELOG.md +182 -10
  2. {sourcecode-2.5.3 → sourcecode-2.5.5}/PKG-INFO +1 -1
  3. {sourcecode-2.5.3 → sourcecode-2.5.5}/pyproject.toml +1 -1
  4. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/architecture_analyzer.py +136 -23
  6. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/classifier.py +4 -2
  7. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/java.py +27 -10
  8. sourcecode-2.5.5/src/sourcecode/reconciliation.py +346 -0
  9. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/repository_ir.py +255 -39
  10. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/spring_impact.py +157 -17
  11. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/summarizer.py +56 -15
  12. {sourcecode-2.5.3 → sourcecode-2.5.5}/.github/workflows/build-windows.yml +0 -0
  13. {sourcecode-2.5.3 → sourcecode-2.5.5}/.gitignore +0 -0
  14. {sourcecode-2.5.3 → sourcecode-2.5.5}/.ruff.toml +0 -0
  15. {sourcecode-2.5.3 → sourcecode-2.5.5}/CONTRIBUTING.md +0 -0
  16. {sourcecode-2.5.3 → sourcecode-2.5.5}/LICENSE +0 -0
  17. {sourcecode-2.5.3 → sourcecode-2.5.5}/README.md +0 -0
  18. {sourcecode-2.5.3 → sourcecode-2.5.5}/SECURITY.md +0 -0
  19. {sourcecode-2.5.3 → sourcecode-2.5.5}/raw +0 -0
  20. {sourcecode-2.5.3 → sourcecode-2.5.5}/scripts/compare_integration_engines.py +0 -0
  21. {sourcecode-2.5.3 → sourcecode-2.5.5}/scripts/customer_smoke_test.sh +0 -0
  22. {sourcecode-2.5.3 → sourcecode-2.5.5}/scripts/generate_jdk_exports.py +0 -0
  23. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/adaptive_scanner.py +0 -0
  24. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/archetype.py +0 -0
  25. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/architecture_summary.py +0 -0
  26. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/ast_extractor.py +0 -0
  27. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/cache.py +0 -0
  28. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/call_surface.py +0 -0
  29. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/caller_metrics.py +0 -0
  30. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/canonical_ir.py +0 -0
  31. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/cir_graphs.py +0 -0
  32. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/cli.py +0 -0
  33. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/code_notes_analyzer.py +0 -0
  34. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/confidence_analyzer.py +0 -0
  35. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/context_cache.py +0 -0
  36. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/context_graph.py +0 -0
  37. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/context_scorer.py +0 -0
  38. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/context_summarizer.py +0 -0
  39. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/contract_model.py +0 -0
  40. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/contract_pipeline.py +0 -0
  41. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/coverage_parser.py +0 -0
  42. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/dependency_analyzer.py +0 -0
  43. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/__init__.py +0 -0
  44. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/base.py +0 -0
  45. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/csproj_parser.py +0 -0
  46. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/dart.py +0 -0
  47. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/dotnet.py +0 -0
  48. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/elixir.py +0 -0
  49. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/go.py +0 -0
  50. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/heuristic.py +0 -0
  51. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/hybrid.py +0 -0
  52. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/jvm_ext.py +0 -0
  53. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/nodejs.py +0 -0
  54. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/parsers.py +0 -0
  55. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/php.py +0 -0
  56. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/project.py +0 -0
  57. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/python.py +0 -0
  58. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/ruby.py +0 -0
  59. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/rust.py +0 -0
  60. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/systems.py +0 -0
  61. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/terraform.py +0 -0
  62. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/detectors/tooling.py +0 -0
  63. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/doc_analyzer.py +0 -0
  64. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/dynamic_argument_surface.py +0 -0
  65. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/endpoint_literals.py +0 -0
  66. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/endpoint_metrics.py +0 -0
  67. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/entrypoint_classifier.py +0 -0
  68. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/env_analyzer.py +0 -0
  69. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/error_schema.py +0 -0
  70. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/evidence_provider.py +0 -0
  71. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/explain.py +0 -0
  72. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/file_chunker.py +0 -0
  73. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/file_classifier.py +0 -0
  74. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/format_contract.py +0 -0
  75. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/fqn_utils.py +0 -0
  76. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/git_analyzer.py +0 -0
  77. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/graph_analyzer.py +0 -0
  78. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/graph_evidence.py +0 -0
  79. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/hibernate_strat.py +0 -0
  80. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/jdk_exports.py +0 -0
  81. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/license.py +0 -0
  82. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/mcp/__init__.py +0 -0
  83. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  84. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  85. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  86. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  87. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  88. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/mcp/orchestrator.py +0 -0
  89. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/mcp/registry.py +0 -0
  90. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/mcp/runner.py +0 -0
  91. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/mcp/server.py +0 -0
  92. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/mcp_nudge.py +0 -0
  93. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/metrics_analyzer.py +0 -0
  94. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/migrate_check.py +0 -0
  95. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/openapi_surface.py +0 -0
  96. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/output_budget.py +0 -0
  97. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/path_filters.py +0 -0
  98. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/pr_comment_renderer.py +0 -0
  99. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/pr_impact.py +0 -0
  100. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/prepare_context.py +0 -0
  101. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/progress.py +0 -0
  102. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/ranking_engine.py +0 -0
  103. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/redactor.py +0 -0
  104. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/relevance_scorer.py +0 -0
  105. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/rename_refactor.py +0 -0
  106. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/repo_classifier.py +0 -0
  107. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/__init__.py +0 -0
  108. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/context.py +0 -0
  109. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/errors.py +0 -0
  110. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/executor.py +0 -0
  111. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/planner.py +0 -0
  112. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/query.py +0 -0
  113. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/request.py +0 -0
  114. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/resolution.py +0 -0
  115. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/result.py +0 -0
  116. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/retriever.py +0 -0
  117. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/runtime.py +0 -0
  118. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps.py +0 -0
  119. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  120. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_graph.py +0 -0
  121. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_impact.py +0 -0
  122. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_intf.py +0 -0
  123. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_struct.py +0 -0
  124. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  125. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/ris.py +0 -0
  126. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/runtime_classifier.py +0 -0
  127. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/scanner.py +0 -0
  128. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/schema.py +0 -0
  129. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/security_config.py +0 -0
  130. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/security_posture.py +0 -0
  131. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/semantic_analyzer.py +0 -0
  132. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/semantic_impact_engine.py +0 -0
  133. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/semantic_integration_engine.py +0 -0
  134. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/semantic_services.py +0 -0
  135. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/serializer.py +0 -0
  136. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/spring_event_topology.py +0 -0
  137. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/spring_findings.py +0 -0
  138. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/spring_model.py +0 -0
  139. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/spring_security_audit.py +0 -0
  140. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/spring_semantic.py +0 -0
  141. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/spring_tx_analyzer.py +0 -0
  142. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/telemetry/__init__.py +0 -0
  143. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/telemetry/config.py +0 -0
  144. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/telemetry/consent.py +0 -0
  145. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/telemetry/events.py +0 -0
  146. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/telemetry/filters.py +0 -0
  147. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/telemetry/transport.py +0 -0
  148. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/tree_utils.py +0 -0
  149. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/type_usage_surface.py +0 -0
  150. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/validation_inference.py +0 -0
  151. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/validation_surface.py +0 -0
  152. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/version_check.py +0 -0
  153. {sourcecode-2.5.3 → sourcecode-2.5.5}/src/sourcecode/workspace.py +0 -0
  154. {sourcecode-2.5.3 → sourcecode-2.5.5}/supabase/functions/README.md +0 -0
  155. {sourcecode-2.5.3 → sourcecode-2.5.5}/supabase/functions/get-license/index.ts +0 -0
  156. {sourcecode-2.5.3 → sourcecode-2.5.5}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  157. {sourcecode-2.5.3 → sourcecode-2.5.5}/supabase/functions/telemetry/index.ts +0 -0
  158. {sourcecode-2.5.3 → sourcecode-2.5.5}/supabase/sql/license_event_ordering.sql +0 -0
  159. {sourcecode-2.5.3 → sourcecode-2.5.5}/supabase/sql/licensing_schema.sql +0 -0
  160. {sourcecode-2.5.3 → sourcecode-2.5.5}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,11 +1,171 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.5] — 2026-07-16
4
+
5
+ **An architecture claim must belong to one program.** Third Evidence
6
+ Reconciliation rule, extending the layer from the impact/extraction surfaces to
7
+ the narrative one.
8
+
9
+ ### Fixed
10
+
11
+ - **A React SPA supplied the "view layer" of a Java MVC claim (SIM-3)**
12
+ (`reconciliation.py`, `architecture_analyzer.py`, `summarizer.py`): keycloak
13
+ read *"MVC pattern with controller, model, view layers"* — the `view` was
14
+ `js/apps/*/src/components` (87 `.tsx`, zero Java), half the `controller` layer
15
+ was the Kubernetes operator's reconcile controllers, and both sat beside a
16
+ 1088-file Java `model`. Directory names are language-blind, so one pattern was
17
+ assembled across two separate programs; P1-D's mass gate cannot catch it,
18
+ because the SPA has genuine mass.
19
+
20
+ New rule **`layer_language_coherence`** — the layer table cross-checked
21
+ against the file-language census. A layer is incoherent iff it holds ZERO
22
+ files of the repo's runtime and at least one file of another: it is a
23
+ different program, not a facet of this architecture. Runtimes are grouped by
24
+ FAMILY (`.java`/`.kt`/`.scala`/`.groovy` = one JVM program), so a Kotlin view
25
+ beside a Java model is never a contradiction. A layer holding no runtime files
26
+ at all (templates, assets) is never flagged — that carve-out is what keeps a
27
+ genuine Spring-MVC `templates/` view intact. Patterns describing repo
28
+ COMPOSITION rather than one program's internals (`fullstack`,
29
+ `microservices`, `monorepo`) are exempt: spanning runtimes is their point.
30
+ The dominant runtime is taken from the whole repo, never from the matched
31
+ layers — which directories match a keyword table is the thing under
32
+ suspicion, so it cannot also be the arbiter. Applied to BOTH duplicated layer
33
+ tables (architecture prose + headline), since fixing one leaves the other
34
+ lying.
35
+
36
+ - **"core" minted an onion/hexagonal domain layer (P1-D residue)**
37
+ (`architecture_analyzer.py`, `summarizer.py`): P1-D removed the keywords that
38
+ do not attest their role (`api`, `store`, `db`) from the layered/fullstack/mvc
39
+ tables but left `core` in onion/hexagonal. `core` names a module's importance,
40
+ never a domain layer, and nearly every large JVM repo has one: keycloak's
41
+ `core/` (623 Java files) plus a `representations/adapters/config` package read
42
+ *"Onion Architecture with domain, adapters layers"*.
43
+
44
+ - **Hexagonal was claimed without ports (P1-D residue)**
45
+ (`architecture_analyzer.py`, `summarizer.py`): a pattern must show its
46
+ defining trait, not just any two of its layers — the mechanism that already
47
+ requires a `view` for MVC. Hexagonal is literally "Ports and Adapters", yet
48
+ `models/` + `adapters/` alone was minting it; onion now requires its domain
49
+ centre. The headline's table enforces required keys too, so it can no longer
50
+ contradict the architecture prose.
51
+
52
+ Validation across 10 field-test repos through the real CLI: keycloak's false
53
+ MVC claim became *"Monorepo workspace with apps, packages layers"* — verified
54
+ true in source (`js/apps/{admin-ui,account-ui}`, `js/libs/*`,
55
+ `pnpm-workspace.yaml`). Broadleaf (layered, all four layers), petclinic,
56
+ jobrunr, neo4j, openmrs, eureka, alfresco, ofbiz and jenkins: byte-identical
57
+ prose, zero collateral. 18 new tests; suite 3440 passed.
58
+
59
+ ## [2.5.4] — 2026-07-16
60
+
61
+ **Evidence reconciliation + the endpoint axis.** The keycloak real-use simulacro
62
+ confirmed the caller graph (28/28 verified callers, 0 false positives) and exposed
63
+ the remaining lie: HTTP exposure. The impact chain could contain a REST resource
64
+ while the endpoint index had no route for it, and nothing compared the two views
65
+ before answering. This release introduces the Evidence Reconciliation Layer —
66
+ consistency rules that run BETWEEN evidence and narrative — and closes the
67
+ extraction gaps its first two rules uncovered, including Java `record` support.
68
+
69
+ ### Added
70
+
71
+ - **Evidence Reconciliation Layer** (`reconciliation.py`, new module): rules are pure
72
+ functions over evidence-view sets that fire only on a *provable* self-contradiction
73
+ between two views — never on absence alone — and downgrade confidence while naming
74
+ the gap. Two rules ship:
75
+ - **`endpoint_coverage`** (SIM-2, `spring_impact.py`): a class in the call chain
76
+ declares HTTP handler methods (open-standard Spring MVC / JAX-RS annotations) but
77
+ the endpoint index has NO modeled route for it → the endpoint list provably
78
+ under-reports exposure. Emits `blind_spots: ["endpoint_coverage_partial"]`, caps
79
+ confidence at medium (callers stay verified — only the endpoint axis is
80
+ incomplete), names the unmapped classes, and carries the structured finding in
81
+ `metadata.reconciliation`. keycloak `RedirectUtils#areWildcardsAllowed` had
82
+ reported *"2 endpoints exposed"* at confidence=high while the OAuth authorization
83
+ surface sat invisible in its own verified chain. Fires only on total index
84
+ absence — a controller contributing a subset of its endpoints is the intentional
85
+ method-precision rule, not a contradiction. Zero false positives across
86
+ Broadleaf/petclinic/openmrs/eureka.
87
+ - **`parse_coverage`** (P1-E, `repository_ir.py`): a Java file whose comment/string-
88
+ masked source declares a type but yielded ZERO symbols is silently invisible to
89
+ the graph, callers and endpoints. Now an `analysis_gaps` entry
90
+ (`area: parse_coverage`, impact high, file list); impact-chain consumes it — an
91
+ empty blast radius gains `blind_spots: ["parse_coverage"]` + confidence low (a
92
+ caller could live in an unmodeled file), a not-found symbol gains a hint that it
93
+ may be defined in one. Non-empty results stay untouched. On first contact with
94
+ real repos this rule *discovered* the two extraction defects below.
95
+
96
+ ### Fixed
97
+
98
+ - **Java `record` declarations produced no symbols (P1-F)** (`repository_ir.py`):
99
+ `public record X(...)` was not in the type-declaration grammar — 52 whole files
100
+ invisible in keycloak alone (protostream entities, connection providers), on any
101
+ modern-Java repo. Records now extract as `type="class"` / `symbol_kind="record"`
102
+ (existing consumers unaffected: class-level scope falls through the `#`-heuristic),
103
+ carry the component list in the signature, resolve component types into
104
+ `imports_used`, and register components as implicit final fields in `field_types`
105
+ so `component.method()` receivers bind like ordinary injected fields. Multiline
106
+ record headers ride the existing continuation-join pass. `record` stays a
107
+ contextual keyword: binding requires `Name(` plus a body brace, so identifiers
108
+ named `record` never match. keycloak: 131 record symbols now in the graph.
109
+
110
+ - **Declarations glued to comments were silently dropped (P1-G)**
111
+ (`repository_ir.py`): any line containing a block-comment marker was discarded
112
+ whole, erasing real declarations — `public class Proc /* implements ... */{`
113
+ (openmrs `HL7InQueueProcessor`) and `*/public class X {` (keycloak
114
+ `JWKSServerUtils`) extracted nothing. Fixing that naively exposed the deeper bug:
115
+ the marker check ignored string literals, so `@Consumes("*/*")` (eureka
116
+ `DiscoveryJerseyProvider`) would poison the comment state and swallow the rest of
117
+ the file. New `_scan_line_comments` is string-literal-aware — markers inside
118
+ strings are code, block-comment spans and `//` tails are removed, the residue is
119
+ processed. Fast-path gated: lines without a marker pay nothing (openmrs 2.4s /
120
+ keycloak 13.9s cold builds, unchanged). Symbols gained: eureka +17, openmrs +22,
121
+ keycloak +38.
122
+
123
+ - **JAX-RS sub-resource locator chains under-resolved (SIM-1)** (`repository_ir.py`):
124
+ keycloak's OAuth authorization endpoint had ZERO endpoints and 39 endpoints across
125
+ 25 classes collapsed to path `/`. Two precise gaps, not a missing resolver:
126
+ (a) a locator declaring `Object` while its body does `return new X(...)`
127
+ (`@Path("auth") public Object auth() { return new AuthorizationEndpoint(...); }`)
128
+ never bound the child — `_extract_symbols` now records instantiated return types
129
+ per method and the locator map falls back to them when the declared type is
130
+ unusable, but only when UNAMBIGUOUS (a multi-branch locator like keycloak's
131
+ `resolveExtension` stays unresolved); (b) a chain whose head is dynamically
132
+ mounted (`getProtocol` resolves the protocol service via an SPI factory) was
133
+ dropped whole by the impl-duplicate guard — `_resolve_jaxrs_prefixes` now splits
134
+ rooted vs unrooted compositions: rooted still wins (impl-duplicate short paths
135
+ still suppressed), and when nothing is rooted the RELATIVE composition is emitted:
136
+ `GET /auth`, `POST /token`, `GET /userinfo` instead of an undifferentiated `/`.
137
+ keycloak: `AuthorizationEndpoint` 0→2 endpoints, path-`/` routes 39→10 (all
138
+ genuine roots); eureka/petclinic/Broadleaf route surfaces unchanged. With the
139
+ index fixed, the `endpoint_coverage` blind spot on the simulacro query
140
+ self-clears: confidence=high, `blind_spots: []`, risk critical with the real
141
+ endpoint set — the two evidence views now agree.
142
+
143
+ - **"Jakarta EE" over-label (P1-C)** (`detectors/java.py`, `classifier.py`): using
144
+ the JAX-RS spec (`javax.ws.rs-api`/`jakarta.ws.rs`/resteasy coordinates, or
145
+ `@Path`/`@GET` annotations) labeled the repo "Jakarta EE" — the platform — so
146
+ eureka (Guice) and keycloak (Quarkus) read as Jakarta EE application servers. The
147
+ spec now labels "JAX-RS"; only an umbrella artifact (`javaee-api`,
148
+ `jakartaee-api`, `jakarta.platform`) claims the platform. Boot 3 apps pulling
149
+ individual `jakarta.*` specs transitively are NOT relabeled (pinned by test —
150
+ the previously suggested "require multiple specs" fix would have mislabeled
151
+ every Boot 3 app).
152
+
153
+ - **Hallucinated architecture layers (P1-D)** (`architecture_analyzer.py`,
154
+ `summarizer.py`): directory-name keyword matching asserted layers with no code
155
+ mass behind them (neo4j: "controller" from 0.1%-mass dirs) and from keywords that
156
+ don't attest the role (`api`→controller at 16–95% mass, `store`→repository,
157
+ `db`→infrastructure). A 1% mass gate (calibrated on real repos — 5% would erase
158
+ Broadleaf's genuine 4.68% controller layer) plus removal of the non-attesting
159
+ keywords, applied to both duplicated tables (analyzer prose + summarizer
160
+ headline). Residue tracked: polyglot monorepos and the modular fallback.
161
+
3
162
  ## [2.5.3] — 2026-07-16
4
163
 
5
164
  **impact-chain caller resolution — three false-confident zeros closed.** All three
6
165
  defects produced the same dangerous shape: a method-scoped query returning
7
166
  `0 callers, risk=low, confidence=high, "isolated"` for a method with real call sites
8
- in source. Continuation of the 12-repo adversarial audit that drove 2.5.2.
167
+ in source. Continuation of the 12-repo adversarial audit that drove 2.5.2. A fourth
168
+ fix removes the reporting redundancy the third one introduced.
9
169
 
10
170
  ### Fixed
11
171
 
@@ -55,15 +215,27 @@ in source. Continuation of the 12-repo adversarial audit that drove 2.5.2.
55
215
  blasts): the guard fires on 42% of empty results, spot-checked true — static utility
56
216
  callers, annotation attribute uses, ORM/serialization setters.
57
217
 
58
- ### Known limitations
59
-
60
- - P1-B2's method-level static edges are **added**, not substituted for the class-level
61
- ones, so a static caller is reported at both granularities (`X` and `X#m`) — redundant,
62
- though each statement is true. Superseding was implemented first and measured wrong:
63
- body facts cover method bodies only, so a static call in a field initializer or static
64
- block yields no atom, and dropping the class edge there erased real coupling
65
- (`BroadleafCurrencyUtils` lost 1325→561 transitive reach and all 20 affected endpoints).
66
- Report-level de-duplication is queued in the backlog; no reach is lost today.
218
+ - **A static caller was reported at two granularities (P1-B3)** (`spring_impact.py`): the
219
+ edges P1-B2 adds are additive by design — the coarse class edge and the precise method
220
+ edge are both kept, because a static call in a field initializer or static block yields
221
+ no body fact, so only the class edge carries it (superseding it was implemented,
222
+ measured, and reverted: `BroadleafCurrencyUtils` lost 1325→561 transitive reach and all
223
+ 20 affected endpoints). The cost was that one caller landed in the report twice, as `X`
224
+ and as `X#m` 153 direct entries for 75 unique caller classes on Broadleaf. Each entry
225
+ is true, but the pair reads as two callers and `len(direct_callers)` double-counted it
226
+ into `_compute_risk`. The reported and scored lists now collapse the pair (the class
227
+ entry is dropped when a method entry of the same class is present; a class entry with no
228
+ method sibling is kept). This is a view, not an edge deletion: endpoint and finding
229
+ mapping still consume both granularities, since a class node reaches every handler of a
230
+ controller while a method node reaches only its own. The hub-cap guard keeps counting
231
+ raw nodes deliberately — it guards BFS cost, and BFS does expand from both. Verified by
232
+ A/B over Broadleaf/openmrs/petclinic: caller **class** sets, endpoints, confidence,
233
+ blind spots and findings identical on every query; entries drop only where the duplicate
234
+ existed (Broadleaf 153→118 direct, 1300→1206 indirect, 21 endpoints unchanged; openmrs
235
+ `ValidateUtil` 107→55 — every one of its 55 caller classes had been listed twice); risk
236
+ de-inflates only below the caller-score cap (petclinic `OwnerRepository` 57.0→51.0, level
237
+ still critical). `ask impact` / `retrieve` were unaffected — they already normalize
238
+ callers to class level.
67
239
 
68
240
  ## [2.5.2] — 2026-07-14
69
241
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.3
3
+ Version: 2.5.5
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "2.5.3"
7
+ version = "2.5.5"
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"
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "2.5.3"
7
+ __version__ = "2.5.5"
@@ -4,6 +4,8 @@ import re
4
4
  from pathlib import Path
5
5
  from typing import Literal, Optional
6
6
 
7
+ from sourcecode.reconciliation import incoherent_layers
8
+
7
9
  from sourcecode.schema import (
8
10
  ArchitectureAnalysis,
9
11
  ArchitectureDomain,
@@ -31,7 +33,14 @@ _TOOLING_PREFIXES = (
31
33
  "dist/",
32
34
  "build/",
33
35
  )
34
- _SRC_TRANSPARENT = {"src", "lib", "app", "pkg"}
36
+ # Structural wrappers to walk THROUGH when looking for a module: they are build
37
+ # layout, never a module name. "main" and the language dirs come from the Maven /
38
+ # Gradle source-set convention (src/main/java/...) — without them a Maven repo with
39
+ # no other signal offers "main" as one of its modules (P1-D, spring-petclinic).
40
+ _SRC_TRANSPARENT = {
41
+ "src", "lib", "app", "pkg",
42
+ "main", "java", "kotlin", "scala", "groovy",
43
+ }
35
44
  _CODE_EXTENSIONS = {
36
45
  ".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs",
37
46
  ".go", ".java", ".kt", ".rs", ".rb",
@@ -56,7 +65,13 @@ _BENCHMARK_DIRS: frozenset[str] = frozenset({
56
65
  "sandbox",
57
66
  })
58
67
  _DOCS_DIRS: frozenset[str] = frozenset({"docs", "doc", "documentation", "wiki"})
59
- _TOOLING_DIRS: frozenset[str] = frozenset({"scripts", "script", "tools", "tool", "ci"})
68
+ # Tooling/CI config, never runtime architecture. The dot-dirs are conventional: a
69
+ # leading dot marks tooling config across ecosystems, and without them `.github`
70
+ # was offered as a neo4j "module" alongside community/ and packaging/ (P1-D).
71
+ _TOOLING_DIRS: frozenset[str] = frozenset({
72
+ "scripts", "script", "tools", "tool", "ci",
73
+ ".github", ".circleci", ".mvn", ".idea", ".vscode",
74
+ })
60
75
  # All dirs that are not part of the runtime source architecture
61
76
  _NON_SOURCE_DIRS: frozenset[str] = _TEST_DIRS | _BENCHMARK_DIRS | _DOCS_DIRS | _TOOLING_DIRS | _ASSET_DIRS
62
77
 
@@ -126,8 +141,14 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
126
141
  "application": ["application", "usecases", "usecase"],
127
142
  "infrastructure": ["infrastructure", "infra", "adapters", "persistence"],
128
143
  },
144
+ # P1-D residue (found by measurement, 2026-07-16): "core" belongs with
145
+ # "api"/"store"/"db" — it names a module's importance ("the main module"),
146
+ # never an onion/hexagonal DOMAIN layer, and nearly every large JVM repo has
147
+ # one. With it, keycloak's `core/` (623 Java files) + a
148
+ # `representations/adapters/config` package read "Onion Architecture with
149
+ # domain, adapters layers". Kept are the names that state their role.
129
150
  "onion": {
130
- "domain": ["domain", "core"],
151
+ "domain": ["domain"],
131
152
  "application": ["application", "usecases"],
132
153
  "ports": ["ports", "interfaces"],
133
154
  "adapters": ["adapters", "secondary"],
@@ -137,16 +158,30 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
137
158
  "model": ["model", "models", "entity", "entities", "domain"],
138
159
  "view": ["views", "templates", "pages", "components"],
139
160
  },
161
+ # P1-D: a keyword may only name a layer it actually ATTESTS. The dropped
162
+ # synonyms below are naming conventions that say nothing about a layer role,
163
+ # and they were the engine of the fabricated claims — mass alone cannot filter
164
+ # them out, because in the repos they mislabel they are large:
165
+ # * "api" — a Java `foo/api/` package is a public API surface (interfaces),
166
+ # not an HTTP controller layer. It manufactured neo4j's controller layer
167
+ # (925 files, 16.5%, and neo4j has no controller dir at all) and openmrs's
168
+ # (821 files, 94.8% — that is `org/openmrs/api/`, the service API).
169
+ # * "store"/"storage" — a storage ENGINE is not a repository/DAO layer
170
+ # (neo4j's `store` is exactly that).
171
+ # * "db"/"database" — a database package is not an infrastructure layer.
172
+ # Kept are the names that state their role: controller/handlers/routes/endpoints,
173
+ # repository/repo/dao, infra/infrastructure/persistence. Broadleaf keeps all four
174
+ # of its layers on those alone.
140
175
  "layered": {
141
- "controller": ["controller", "controllers", "api", "routes", "handlers", "endpoints"],
176
+ "controller": ["controller", "controllers", "routes", "handlers", "endpoints"],
142
177
  "service": ["service", "services", "usecase", "usecases", "application"],
143
- "repository": ["repository", "repositories", "repo", "repos", "store", "storage", "dao"],
144
- "infrastructure": ["infra", "infrastructure", "persistence", "db", "database"],
178
+ "repository": ["repository", "repositories", "repo", "repos", "dao"],
179
+ "infrastructure": ["infra", "infrastructure", "persistence"],
145
180
  },
146
181
  "hexagonal": {
147
182
  "port": ["port", "ports", "interface", "interfaces"],
148
183
  "adapter": ["adapter", "adapters"],
149
- "domain": ["domain", "core", "model", "models"],
184
+ "domain": ["domain", "model", "models"], # "core" dropped — see onion
150
185
  },
151
186
  "monorepo": {
152
187
  "apps": ["apps", "applications"],
@@ -154,7 +189,11 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
154
189
  },
155
190
  "fullstack": {
156
191
  "frontend": ["frontend", "client", "web", "ui", "pages", "components", "app"],
157
- "backend": ["backend", "server", "api", "services"],
192
+ # P1-D: "api" dropped — a `foo/api/` package is a public API surface, not
193
+ # evidence of a backend TIER. With it, openmrs (94.8% `org/openmrs/api/`)
194
+ # and jobrunr fell straight from a fabricated "layered" into a fabricated
195
+ # "fullstack" instead. Same conflation, same fix.
196
+ "backend": ["backend", "server", "services"],
158
197
  },
159
198
  }
160
199
 
@@ -165,8 +204,37 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
165
204
  # it; without it the pattern falls through to layered / a weaker match.
166
205
  _PATTERN_REQUIRED_KEYS: dict[str, frozenset[str]] = {
167
206
  "mvc": frozenset({"view"}),
207
+ # Same principle as MVC/view — a pattern must show its DEFINING trait, not
208
+ # just any two of its layers. Hexagonal is literally "Ports and Adapters":
209
+ # a `models/` package next to an `adapters/` package is not one, and that
210
+ # pair alone was enough to label keycloak "Hexagonal Architecture". Onion's
211
+ # defining trait is the domain at the centre.
212
+ "hexagonal": frozenset({"port", "adapter"}),
213
+ "onion": frozenset({"domain"}),
168
214
  }
169
215
 
216
+ # Minimum share of source files a matched layer must hold to count as a LAYER
217
+ # (P1-D, neo4j field test). Matching was pure name PRESENCE over the set of every
218
+ # directory name in the repo, so one incidental dir minted a whole layer: neo4j — a
219
+ # graph engine with no controller, repository or infrastructure directory anywhere —
220
+ # reported "Layered Architecture with controller, service, repository, infrastructure
221
+ # layers", synthesized from `api`, `service` (7 files), `store`/`storage` and
222
+ # `database`. Presence does not scale: the bigger the repo, the likelier some
223
+ # directory somewhere matches a keyword, so large systems were the most confidently
224
+ # mislabelled.
225
+ #
226
+ # A layer is a place where a real share of the code lives. Same reasoning the
227
+ # archetype engine already applies to the same question ("a 0.3% `api/` dir does not
228
+ # manufacture a controller layer", _architectural_style).
229
+ #
230
+ # Calibrated against the field-test repos rather than copied from archetype's 5%,
231
+ # which scores a web+service+repository triplet and is too coarse to erase a single
232
+ # layer with: Broadleaf's controller layer is a real 4.68% (it has a literal
233
+ # `controller/` dir) and 5% deleted it. The phantoms all sit an order of magnitude
234
+ # lower — jenkins 0.72% (`handlers`, and it has no HTTP controllers at all), ofbiz
235
+ # 0.19%, neo4j's service layer 7 files at 0.12% — so 1% separates them cleanly.
236
+ _LAYER_MIN_SHARE = 0.01
237
+
170
238
  # Higher value = wins when score ties
171
239
  _PATTERN_PRIORITY: dict[str, int] = {
172
240
  "cqrs": 8,
@@ -555,24 +623,62 @@ class ArchitectureAnalyzer:
555
623
  if not source_paths:
556
624
  return "unknown", []
557
625
 
558
- dir_names: set[str] = set()
559
- for p in source_paths:
560
- parts = p.replace("\\", "/").split("/")
561
- for part in parts[:-1]:
562
- dir_names.add(part.lower())
626
+ # Index each directory NAME to the source files under it, so a matched layer
627
+ # can be weighed by the code it actually holds instead of counted as present
628
+ # (P1-D). Built once; every pattern below reads it.
629
+ dir_files: dict[str, set[int]] = {}
630
+ for i, p in enumerate(source_paths):
631
+ for part in p.replace("\\", "/").split("/")[:-1]:
632
+ dir_files.setdefault(part.lower(), set()).add(i)
633
+ dir_names: set[str] = set(dir_files)
634
+ total_files = len(source_paths)
635
+
636
+ def _layer_files(matched_dirs: list[str]) -> set[int]:
637
+ files: set[int] = set()
638
+ for d in matched_dirs:
639
+ files |= dir_files.get(d, set())
640
+ return files
563
641
 
564
642
  # 1. Classical keyword-based pattern matching
565
643
  best_pattern = ""
566
644
  best_score = 0
567
645
  best_priority = -1
568
646
  best_matched: dict[str, list[str]] = {}
647
+ best_layer_files: dict[str, set[int]] = {}
569
648
 
570
649
  for pattern_name, layer_keys in LAYER_PATTERNS.items():
571
650
  matched: dict[str, list[str]] = {}
651
+ layer_files: dict[str, set[int]] = {}
572
652
  for layer_key, keywords in layer_keys.items():
573
653
  matched_dirs = [d for d in dir_names if d in keywords]
574
- if matched_dirs:
575
- matched[layer_key] = matched_dirs
654
+ if not matched_dirs:
655
+ continue
656
+ # P1-D: a layer is where a real share of the code lives. An incidental
657
+ # directory that merely matches a keyword is not one — neo4j's 7-file
658
+ # `service/` dir (0.12%) never made it a service layer.
659
+ files = _layer_files(matched_dirs)
660
+ if total_files and len(files) / total_files < _LAYER_MIN_SHARE:
661
+ continue
662
+ matched[layer_key] = matched_dirs
663
+ layer_files[layer_key] = files
664
+
665
+ # SIM-3: a layer must belong to the same PROGRAM as the rest of the
666
+ # claim. Directory names are language-blind, so on a polyglot
667
+ # monorepo one pattern was assembled across two subsystems: keycloak
668
+ # read "MVC with controller, model, view layers" where `view` was a
669
+ # React SPA (87 .tsx, zero Java) sitting beside a 1088-file Java
670
+ # `model`. Mass cannot catch this — the SPA has genuine mass. The
671
+ # reconciliation rule drops layers owned by a different runtime;
672
+ # template/asset-only layers are untouched, so a real Spring-MVC
673
+ # `templates/` view survives.
674
+ for _bad in incoherent_layers(
675
+ pattern_name,
676
+ {k: [source_paths[i] for i in v] for k, v in layer_files.items()},
677
+ source_paths,
678
+ ):
679
+ matched.pop(_bad, None)
680
+ layer_files.pop(_bad, None)
681
+
576
682
  # A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
577
683
  required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
578
684
  if required and not required.issubset(matched.keys()):
@@ -584,18 +690,13 @@ class ArchitectureAnalyzer:
584
690
  best_priority = priority
585
691
  best_pattern = pattern_name
586
692
  best_matched = matched
693
+ best_layer_files = layer_files
587
694
 
588
695
  if best_score >= 2:
589
696
  layer_confidence: Literal["high", "medium", "low"] = "medium" if best_score >= 3 else "low"
590
697
  layers: list[ArchitectureLayer] = []
591
698
  for layer_key, matched_dirs in best_matched.items():
592
- matched_files = [
593
- p for p in source_paths
594
- if any(
595
- seg.lower() in matched_dirs
596
- for seg in p.replace("\\", "/").split("/")[:-1]
597
- )
598
- ]
699
+ matched_files = [source_paths[i] for i in sorted(best_layer_files[layer_key])]
599
700
  layers.append(ArchitectureLayer(
600
701
  name=layer_key,
601
702
  pattern=best_pattern,
@@ -716,6 +817,14 @@ class ArchitectureAnalyzer:
716
817
  - orchestration: cli.py, main.py, *_handler.py, *_controller.py
717
818
  - processing: *_analyzer.py, *_processor.py, *_parser.py, *_detector.py, *_scanner.py
718
819
  - data: schema.py, model.py, *_repository.py, *_serializer.py, config.py, …
820
+
821
+ P1-D: weighed by share, like the directory-name patterns. Matching on file
822
+ stems alone let a handful of incidentally-named files mint logical layers:
823
+ neo4j's "orchestration, processing, data" rested on ~6 files out of 5616
824
+ (0.05% each) — one `Main.java`, a `Config.java`, a `Serializer.java`, and two
825
+ `Server.java` that are model classes under `topology/model/`, "orchestration"
826
+ only because of their stem. Three layers named off 0.1% of a codebase is a
827
+ description of nothing.
719
828
  """
720
829
  layer_files: dict[str, list[str]] = {"orchestration": [], "processing": [], "data": []}
721
830
  for p in paths:
@@ -728,7 +837,11 @@ class ArchitectureAnalyzer:
728
837
  layer_files[layer].append(p)
729
838
  break
730
839
 
731
- non_empty = {k: v for k, v in layer_files.items() if v}
840
+ total = len(paths)
841
+ non_empty = {
842
+ k: v for k, v in layer_files.items()
843
+ if v and (not total or len(v) / total >= _LAYER_MIN_SHARE)
844
+ }
732
845
  if len(non_empty) >= 2:
733
846
  return "layered", [
734
847
  ArchitectureLayer(name=k, pattern="layered", files=v, confidence="low")
@@ -25,7 +25,9 @@ _API_FRAMEWORKS = {
25
25
  "Micronaut",
26
26
  "Vert.x",
27
27
  "Quarkus",
28
- "Jakarta EE", # JAX-RS / Jakarta REST — pure JAX-RS projects must not fall to "unknown"
28
+ "JAX-RS", # Jakarta REST — pure JAX-RS projects must not fall to "unknown"
29
+ "Jakarta EE", # platform umbrella artifact (jakartaee-api); implies a web profile
30
+ "Java EE", # javax.* umbrella artifact (javaee-api)
29
31
  "Laravel",
30
32
  "Symfony",
31
33
  "Ktor",
@@ -42,7 +44,7 @@ _API_STACKS = {"python", "go", "java", "php", "ruby", "dotnet", "kotlin", "scala
42
44
  # guard because they are repo-wide (no locatable minority module):
43
45
  # * a jakarta.ws.rs / javax.ws.rs-api coordinate pinned in a BOM/parent pom's
44
46
  # <dependencyManagement> (a version pin, not usage), and
45
- # * a source-annotation "Jakarta EE" marker synthesized from @Path resources
47
+ # * a source-annotation "JAX-RS" marker synthesized from @Path resources
46
48
  # (detected_via carries no file path).
47
49
  # So on a repo large enough to HAVE a distinct center of gravity, require the API
48
50
  # surface to occupy a real share of the code (or the dominant module) before we
@@ -185,17 +185,22 @@ class JavaDetector(AbstractDetector):
185
185
  # build.gradle files are not scanned at all — yet it ships 18 @Path resource
186
186
  # classes. Without this the headline read "Java library / package" while the
187
187
  # endpoints command listed a full JAX-RS API. When the annotation scan found
188
- # JAX-RS entry points and no Jakarta-EE framework was inferred from the
188
+ # JAX-RS entry points and no JAX-RS framework was inferred from the
189
189
  # manifest, surface it so the stack (and project_type) reflect the REST API
190
190
  # the endpoints already prove. Structural (VAI): reasons from the JAX-RS
191
191
  # entry points found in source, not from a vendor dependency-name allowlist.
192
192
  # detected_via carries no path form, so it is treated as repo-wide evidence
193
193
  # (never localized to an adapter submodule).
194
- if not any(f.name == "Jakarta EE" for f in frameworks) and any(
194
+ # P1-C: @Path/@GET resources prove JAX-RS the one spec they belong to —
195
+ # and nothing about the rest of the Jakarta EE platform. Eureka (Jersey 1.x
196
+ # + Guice, no EE container) read "Java rest api using Jakarta EE" on this
197
+ # path alone. The API framing this exists to deliver is unchanged; only the
198
+ # claim narrows to what the annotations attest.
199
+ if not any(f.name == "JAX-RS" for f in frameworks) and any(
195
200
  ep.kind in ("jax_rs_controller", "jax_rs_provider") for ep in entry_points
196
201
  ):
197
202
  frameworks.append(FrameworkDetection(
198
- name="Jakarta EE", source="annotation", detected_via=["jax_rs_endpoints"],
203
+ name="JAX-RS", source="annotation", detected_via=["jax_rs_endpoints"],
199
204
  ))
200
205
 
201
206
  transactional_classes = self._collect_transactional_classes(context, all_paths)
@@ -436,15 +441,27 @@ class JavaDetector(AbstractDetector):
436
441
  frameworks.append(FrameworkDetection(name="Micronaut", source=source))
437
442
  if "io.vertx" in text or "vertx" in text:
438
443
  frameworks.append(FrameworkDetection(name="Vert.x", source=source))
444
+ # P1-C: name the spec that is actually evidenced, not the platform that
445
+ # contains it. "Jakarta EE" is a platform of ~40 specs; a JAX-RS coordinate
446
+ # attests ONE of them. Claiming the platform from it told neo4j (a graph
447
+ # engine with an HTTP API) and jobrunr (a job library with RESTEasy on the
448
+ # test path) that they were Jakarta EE applications.
449
+ #
450
+ # Only an aggregate platform artifact — the coordinate that IS the platform
451
+ # umbrella — attests the platform itself. Counting distinct jakarta.* specs
452
+ # instead would misfire: Spring Boot 3 pulls jakarta.servlet, .validation and
453
+ # .persistence transitively (Broadleaf ships the first two), so any threshold
454
+ # low enough to catch a real EE app relabels every Boot 3 app.
455
+ if "javaee-api" in text: # javax.* umbrella (pre-EE 9)
456
+ frameworks.append(FrameworkDetection(name="Java EE", source=source))
457
+ if "jakartaee-api" in text or "jakarta.platform" in text or "jakarta.ee" in text:
458
+ frameworks.append(FrameworkDetection(name="Jakarta EE", source=source))
439
459
  if (
440
- "jakarta.ee" in text
441
- or "javax.ws.rs-api" in text # JAX-RS 2.x spec; excludes jsr311-api (1.x API-only jar)
442
- or "jakarta.ws.rs" in text # Jakarta namespace (EE 9+)
443
- or "javaee-api" in text # full Java EE platform
444
- or "jakartaee-api" in text # full Jakarta EE platform
445
- or "resteasy" in text # RESTEasy (JBoss JAX-RS impl)
460
+ "javax.ws.rs-api" in text # JAX-RS 2.x spec; excludes jsr311-api (1.x API-only jar)
461
+ or "jakarta.ws.rs" in text # Jakarta namespace (EE 9+)
462
+ or "resteasy" in text # RESTEasy a JAX-RS implementation
446
463
  ):
447
- frameworks.append(FrameworkDetection(name="Jakarta EE", source=source))
464
+ frameworks.append(FrameworkDetection(name="JAX-RS", source=source))
448
465
  if "mybatis" in text:
449
466
  frameworks.append(FrameworkDetection(name="MyBatis", source=source))
450
467
  if "thymeleaf" in text: