sourcecode 2.5.4__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.4 → sourcecode-2.5.5}/CHANGELOG.md +56 -0
  2. {sourcecode-2.5.4 → sourcecode-2.5.5}/PKG-INFO +1 -1
  3. {sourcecode-2.5.4 → sourcecode-2.5.5}/pyproject.toml +1 -1
  4. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/architecture_analyzer.py +35 -2
  6. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/reconciliation.py +145 -0
  7. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/summarizer.py +25 -5
  8. {sourcecode-2.5.4 → sourcecode-2.5.5}/.github/workflows/build-windows.yml +0 -0
  9. {sourcecode-2.5.4 → sourcecode-2.5.5}/.gitignore +0 -0
  10. {sourcecode-2.5.4 → sourcecode-2.5.5}/.ruff.toml +0 -0
  11. {sourcecode-2.5.4 → sourcecode-2.5.5}/CONTRIBUTING.md +0 -0
  12. {sourcecode-2.5.4 → sourcecode-2.5.5}/LICENSE +0 -0
  13. {sourcecode-2.5.4 → sourcecode-2.5.5}/README.md +0 -0
  14. {sourcecode-2.5.4 → sourcecode-2.5.5}/SECURITY.md +0 -0
  15. {sourcecode-2.5.4 → sourcecode-2.5.5}/raw +0 -0
  16. {sourcecode-2.5.4 → sourcecode-2.5.5}/scripts/compare_integration_engines.py +0 -0
  17. {sourcecode-2.5.4 → sourcecode-2.5.5}/scripts/customer_smoke_test.sh +0 -0
  18. {sourcecode-2.5.4 → sourcecode-2.5.5}/scripts/generate_jdk_exports.py +0 -0
  19. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/adaptive_scanner.py +0 -0
  20. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/archetype.py +0 -0
  21. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/architecture_summary.py +0 -0
  22. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/ast_extractor.py +0 -0
  23. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/cache.py +0 -0
  24. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/call_surface.py +0 -0
  25. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/caller_metrics.py +0 -0
  26. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/canonical_ir.py +0 -0
  27. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/cir_graphs.py +0 -0
  28. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/classifier.py +0 -0
  29. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/cli.py +0 -0
  30. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/code_notes_analyzer.py +0 -0
  31. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/confidence_analyzer.py +0 -0
  32. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/context_cache.py +0 -0
  33. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/context_graph.py +0 -0
  34. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/context_scorer.py +0 -0
  35. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/context_summarizer.py +0 -0
  36. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/contract_model.py +0 -0
  37. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/contract_pipeline.py +0 -0
  38. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/coverage_parser.py +0 -0
  39. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/dependency_analyzer.py +0 -0
  40. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/__init__.py +0 -0
  41. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/base.py +0 -0
  42. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/csproj_parser.py +0 -0
  43. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/dart.py +0 -0
  44. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/dotnet.py +0 -0
  45. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/elixir.py +0 -0
  46. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/go.py +0 -0
  47. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/heuristic.py +0 -0
  48. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/hybrid.py +0 -0
  49. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/java.py +0 -0
  50. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/jvm_ext.py +0 -0
  51. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/nodejs.py +0 -0
  52. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/parsers.py +0 -0
  53. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/php.py +0 -0
  54. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/project.py +0 -0
  55. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/python.py +0 -0
  56. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/ruby.py +0 -0
  57. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/rust.py +0 -0
  58. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/systems.py +0 -0
  59. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/terraform.py +0 -0
  60. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/detectors/tooling.py +0 -0
  61. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/doc_analyzer.py +0 -0
  62. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/dynamic_argument_surface.py +0 -0
  63. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/endpoint_literals.py +0 -0
  64. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/endpoint_metrics.py +0 -0
  65. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/entrypoint_classifier.py +0 -0
  66. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/env_analyzer.py +0 -0
  67. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/error_schema.py +0 -0
  68. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/evidence_provider.py +0 -0
  69. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/explain.py +0 -0
  70. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/file_chunker.py +0 -0
  71. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/file_classifier.py +0 -0
  72. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/format_contract.py +0 -0
  73. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/fqn_utils.py +0 -0
  74. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/git_analyzer.py +0 -0
  75. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/graph_analyzer.py +0 -0
  76. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/graph_evidence.py +0 -0
  77. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/hibernate_strat.py +0 -0
  78. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/jdk_exports.py +0 -0
  79. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/license.py +0 -0
  80. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/mcp/__init__.py +0 -0
  81. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  82. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  83. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  84. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  85. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  86. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/mcp/orchestrator.py +0 -0
  87. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/mcp/registry.py +0 -0
  88. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/mcp/runner.py +0 -0
  89. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/mcp/server.py +0 -0
  90. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/mcp_nudge.py +0 -0
  91. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/metrics_analyzer.py +0 -0
  92. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/migrate_check.py +0 -0
  93. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/openapi_surface.py +0 -0
  94. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/output_budget.py +0 -0
  95. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/path_filters.py +0 -0
  96. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/pr_comment_renderer.py +0 -0
  97. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/pr_impact.py +0 -0
  98. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/prepare_context.py +0 -0
  99. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/progress.py +0 -0
  100. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/ranking_engine.py +0 -0
  101. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/redactor.py +0 -0
  102. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/relevance_scorer.py +0 -0
  103. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/rename_refactor.py +0 -0
  104. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/repo_classifier.py +0 -0
  105. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/repository_ir.py +0 -0
  106. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/__init__.py +0 -0
  107. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/context.py +0 -0
  108. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/errors.py +0 -0
  109. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/executor.py +0 -0
  110. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/planner.py +0 -0
  111. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/query.py +0 -0
  112. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/request.py +0 -0
  113. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/resolution.py +0 -0
  114. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/result.py +0 -0
  115. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/retriever.py +0 -0
  116. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/runtime.py +0 -0
  117. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps.py +0 -0
  118. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  119. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_graph.py +0 -0
  120. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_impact.py +0 -0
  121. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_intf.py +0 -0
  122. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_struct.py +0 -0
  123. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  124. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/ris.py +0 -0
  125. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/runtime_classifier.py +0 -0
  126. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/scanner.py +0 -0
  127. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/schema.py +0 -0
  128. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/security_config.py +0 -0
  129. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/security_posture.py +0 -0
  130. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/semantic_analyzer.py +0 -0
  131. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/semantic_impact_engine.py +0 -0
  132. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/semantic_integration_engine.py +0 -0
  133. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/semantic_services.py +0 -0
  134. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/serializer.py +0 -0
  135. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/spring_event_topology.py +0 -0
  136. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/spring_findings.py +0 -0
  137. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/spring_impact.py +0 -0
  138. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/spring_model.py +0 -0
  139. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/spring_security_audit.py +0 -0
  140. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/spring_semantic.py +0 -0
  141. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/spring_tx_analyzer.py +0 -0
  142. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/telemetry/__init__.py +0 -0
  143. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/telemetry/config.py +0 -0
  144. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/telemetry/consent.py +0 -0
  145. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/telemetry/events.py +0 -0
  146. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/telemetry/filters.py +0 -0
  147. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/telemetry/transport.py +0 -0
  148. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/tree_utils.py +0 -0
  149. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/type_usage_surface.py +0 -0
  150. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/validation_inference.py +0 -0
  151. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/validation_surface.py +0 -0
  152. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/version_check.py +0 -0
  153. {sourcecode-2.5.4 → sourcecode-2.5.5}/src/sourcecode/workspace.py +0 -0
  154. {sourcecode-2.5.4 → sourcecode-2.5.5}/supabase/functions/README.md +0 -0
  155. {sourcecode-2.5.4 → sourcecode-2.5.5}/supabase/functions/get-license/index.ts +0 -0
  156. {sourcecode-2.5.4 → sourcecode-2.5.5}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  157. {sourcecode-2.5.4 → sourcecode-2.5.5}/supabase/functions/telemetry/index.ts +0 -0
  158. {sourcecode-2.5.4 → sourcecode-2.5.5}/supabase/sql/license_event_ordering.sql +0 -0
  159. {sourcecode-2.5.4 → sourcecode-2.5.5}/supabase/sql/licensing_schema.sql +0 -0
  160. {sourcecode-2.5.4 → sourcecode-2.5.5}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,61 @@
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
+
3
59
  ## [2.5.4] — 2026-07-16
4
60
 
5
61
  **Evidence reconciliation + the endpoint axis.** The keycloak real-use simulacro
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.4
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.4"
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.4"
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,
@@ -139,8 +141,14 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
139
141
  "application": ["application", "usecases", "usecase"],
140
142
  "infrastructure": ["infrastructure", "infra", "adapters", "persistence"],
141
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.
142
150
  "onion": {
143
- "domain": ["domain", "core"],
151
+ "domain": ["domain"],
144
152
  "application": ["application", "usecases"],
145
153
  "ports": ["ports", "interfaces"],
146
154
  "adapters": ["adapters", "secondary"],
@@ -173,7 +181,7 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
173
181
  "hexagonal": {
174
182
  "port": ["port", "ports", "interface", "interfaces"],
175
183
  "adapter": ["adapter", "adapters"],
176
- "domain": ["domain", "core", "model", "models"],
184
+ "domain": ["domain", "model", "models"], # "core" dropped — see onion
177
185
  },
178
186
  "monorepo": {
179
187
  "apps": ["apps", "applications"],
@@ -196,6 +204,13 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
196
204
  # it; without it the pattern falls through to layered / a weaker match.
197
205
  _PATTERN_REQUIRED_KEYS: dict[str, frozenset[str]] = {
198
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"}),
199
214
  }
200
215
 
201
216
  # Minimum share of source files a matched layer must hold to count as a LAYER
@@ -646,6 +661,24 @@ class ArchitectureAnalyzer:
646
661
  continue
647
662
  matched[layer_key] = matched_dirs
648
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
+
649
682
  # A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
650
683
  required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
651
684
  if required and not required.issubset(matched.keys()):
@@ -34,12 +34,21 @@ Rule registry (extraction):
34
34
  declarations the regex extractor cannot bind, e.g. `public class\n
35
35
  Name` split across lines, Unicode type identifiers).
36
36
 
37
+ Rule registry (architecture):
38
+ layer_language_coherence — a directory-name-matched layer holds NO file in
39
+ the runtime that carries the rest of the claimed pattern, while being
40
+ owned by a DIFFERENT runtime. The layer table and the file-language
41
+ census are contradicting each other: the "architecture" spans two
42
+ subsystems (SIM-3: keycloak's "MVC … view layer" was a React SPA, zero
43
+ Java files, sitting beside a 1088-file Java model).
44
+
37
45
  Never raises. Empty evidence → no findings.
38
46
  """
39
47
  from __future__ import annotations
40
48
 
41
49
  import re
42
50
  from dataclasses import dataclass
51
+ from pathlib import Path
43
52
 
44
53
 
45
54
  @dataclass(frozen=True)
@@ -182,6 +191,142 @@ def _rule_parse_coverage(
182
191
  ]
183
192
 
184
193
 
194
+ # ---------------------------------------------------------------------------
195
+ # Architecture reconciliation (layer table × file-language census)
196
+ # ---------------------------------------------------------------------------
197
+
198
+ # Source extension → RUNTIME FAMILY. Grouping by runtime, not by extension, is
199
+ # what makes the rule correct: a Kotlin `view/` beside Java `model/` is ONE
200
+ # system (same runtime, direct interop) and must not be called a contradiction,
201
+ # while a TypeScript `view/` beside them is a separate program. Extensions
202
+ # absent here (.html/.ftl/.jsp/.css/.xml …) are deliberately unmapped: a
203
+ # template or asset carries no runtime of its own, so it can never contradict a
204
+ # claim — that carve-out is what keeps a genuine Spring-MVC `templates/` view
205
+ # layer intact.
206
+ _RUNTIME_FAMILY: dict[str, str] = {
207
+ ".java": "jvm", ".kt": "jvm", ".kts": "jvm", ".scala": "jvm", ".groovy": "jvm",
208
+ ".js": "js", ".jsx": "js", ".ts": "js", ".tsx": "js", ".mjs": "js", ".cjs": "js",
209
+ ".py": "python",
210
+ ".go": "go",
211
+ ".rs": "rust",
212
+ ".rb": "ruby",
213
+ ".cs": "dotnet",
214
+ ".php": "php",
215
+ ".swift": "swift",
216
+ }
217
+
218
+ # Patterns that describe REPO COMPOSITION rather than one program's internal
219
+ # architecture — their layers are SUPPOSED to span runtimes, so the coherence
220
+ # rule must never fire on them. (mvc/layered/onion/hexagonal/clean are claims
221
+ # about a single program and must hold together in one runtime.)
222
+ _CROSS_RUNTIME_PATTERNS: frozenset[str] = frozenset({
223
+ "fullstack", "microservices", "monorepo",
224
+ })
225
+
226
+
227
+ def runtime_census(layer_files: dict[str, list[str]]) -> dict[str, dict[str, int]]:
228
+ """{layer -> {runtime_family -> file count}} — the language evidence view.
229
+
230
+ Files whose extension carries no runtime (templates, assets, config) are
231
+ simply absent from the census; a layer made only of them yields {}.
232
+ """
233
+ out: dict[str, dict[str, int]] = {}
234
+ for layer, files in layer_files.items():
235
+ counts: dict[str, int] = {}
236
+ for f in files:
237
+ fam = _RUNTIME_FAMILY.get(Path(str(f)).suffix.lower())
238
+ if fam:
239
+ counts[fam] = counts.get(fam, 0) + 1
240
+ out[layer] = counts
241
+ return out
242
+
243
+
244
+ def _rule_layer_language_coherence(
245
+ pattern: str,
246
+ census: dict[str, dict[str, int]],
247
+ repo_runtimes: dict[str, int],
248
+ ) -> list[ReconciliationFinding]:
249
+ """A claimed layer is owned by a different runtime than the repo it describes.
250
+
251
+ The dominant runtime is taken from the WHOLE repo, not from the matched
252
+ layers: which directories happen to match a keyword table is exactly the
253
+ thing under suspicion, so it cannot also be the arbiter. (Measured: on a
254
+ Java backend beside a React SPA, layer-local dominance can invert — the SPA
255
+ out-matches the backend's one matched layer and the JAVA layer gets flagged.)
256
+
257
+ A layer is INCOHERENT iff it holds ZERO files of the repo's runtime AND at
258
+ least one file of another — it is a different program, not a facet of this
259
+ one. A layer with no runtime files at all (templates, assets) is never
260
+ flagged: it does not contradict, it just does not attest.
261
+
262
+ Silent for cross-runtime patterns, whose layers are meant to span runtimes.
263
+ """
264
+ if pattern in _CROSS_RUNTIME_PATTERNS or len(census) < 2:
265
+ return []
266
+ if len(repo_runtimes) < 2:
267
+ return [] # single-runtime repo — nothing to contradict
268
+
269
+ dominant = max(sorted(repo_runtimes), key=lambda f: repo_runtimes[f])
270
+
271
+ incoherent = sorted(
272
+ layer for layer, counts in census.items()
273
+ if not counts.get(dominant) and any(counts.values())
274
+ )
275
+ if not incoherent:
276
+ return []
277
+ return [
278
+ ReconciliationFinding(
279
+ rule="layer_language_coherence",
280
+ symbols=tuple(incoherent),
281
+ detail=(
282
+ f"{len(incoherent)} layer(s) of the '{pattern}' claim hold no "
283
+ f"{dominant} file while the rest of the pattern is {dominant} — "
284
+ "the claim spans two separate programs, so it does not describe "
285
+ "one architecture."
286
+ ),
287
+ )
288
+ ]
289
+
290
+
291
+ def reconcile_architecture_evidence(
292
+ *,
293
+ pattern: str,
294
+ layer_files: dict[str, list[str]],
295
+ repo_files: "list[str]",
296
+ ) -> list[ReconciliationFinding]:
297
+ """Run architecture reconciliation rules. Never raises.
298
+
299
+ Args:
300
+ pattern: candidate pattern name (e.g. "mvc").
301
+ layer_files: {layer -> files matched for that layer}.
302
+ repo_files: every source file considered — supplies the repo's own
303
+ runtime, the arbiter of which layers belong to the claim.
304
+ """
305
+ findings: list[ReconciliationFinding] = []
306
+ try:
307
+ repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
308
+ findings.extend(
309
+ _rule_layer_language_coherence(
310
+ pattern, runtime_census(layer_files), repo_runtimes
311
+ )
312
+ )
313
+ except Exception:
314
+ pass
315
+ return findings
316
+
317
+
318
+ def incoherent_layers(
319
+ pattern: str, layer_files: dict[str, list[str]], repo_files: "list[str]"
320
+ ) -> frozenset[str]:
321
+ """Layer names the coherence rule rejects for `pattern` (convenience view)."""
322
+ out: set[str] = set()
323
+ for f in reconcile_architecture_evidence(
324
+ pattern=pattern, layer_files=layer_files, repo_files=repo_files
325
+ ):
326
+ out.update(f.symbols)
327
+ return frozenset(out)
328
+
329
+
185
330
  def reconcile_extraction_evidence(
186
331
  *,
187
332
  type_decl_files: frozenset[str],
@@ -8,7 +8,8 @@ No API calls — templates applied directly over SourceMap.
8
8
  from pathlib import Path
9
9
  from typing import Any
10
10
 
11
- from sourcecode.architecture_analyzer import _LAYER_MIN_SHARE
11
+ from sourcecode.architecture_analyzer import _LAYER_MIN_SHARE, _PATTERN_REQUIRED_KEYS
12
+ from sourcecode.reconciliation import incoherent_layers
12
13
  from sourcecode.detectors.parsers import load_json_file, load_toml_file
13
14
  from sourcecode.tree_utils import safe_read_text
14
15
  from sourcecode.entrypoint_classifier import is_production_entry_point
@@ -62,7 +63,9 @@ _ARCH_LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
62
63
  "hexagonal": {
63
64
  "port": ["ports", "interfaces"],
64
65
  "adapter": ["adapters"],
65
- "domain": ["domain", "core", "models"],
66
+ # P1-D residue: "core" names a module's importance, not a domain layer
67
+ # (keycloak `core/` = 623 Java files). Keep in step with the analyzer.
68
+ "domain": ["domain", "models"],
66
69
  },
67
70
  "fullstack": {
68
71
  "frontend": ["frontend", "client", "web", "ui", "pages", "components"],
@@ -415,16 +418,33 @@ class ProjectSummarizer:
415
418
  dir_files.setdefault(seg.lower(), set()).add(i)
416
419
  total = len(file_paths)
417
420
 
418
- def _has_mass(keywords: list[str]) -> bool:
421
+ def _layer_paths(keywords: list[str]) -> list[str]:
419
422
  files: set[int] = set()
420
423
  for d in keywords:
421
424
  files |= dir_files.get(d, set())
422
- return bool(files) and (not total or len(files) / total >= _LAYER_MIN_SHARE)
425
+ if not files or (total and len(files) / total < _LAYER_MIN_SHARE):
426
+ return []
427
+ return [file_paths[i] for i in sorted(files)]
423
428
 
424
429
  best_pattern: str | None = None
425
430
  best_score = 0
426
431
  for pattern_name, layer_keys in _ARCH_LAYER_PATTERNS.items():
427
- score = sum(1 for keywords in layer_keys.values() if _has_mass(keywords))
432
+ layer_files: dict[str, list[str]] = {}
433
+ for layer, keywords in layer_keys.items():
434
+ paths = _layer_paths(keywords)
435
+ if paths:
436
+ layer_files[layer] = paths
437
+ # SIM-3: same coherence rule the architecture prose applies — this
438
+ # table is the headline's copy, and a fix applied to only one of the
439
+ # two duplicated tables leaves the other one lying.
440
+ for _bad in incoherent_layers(pattern_name, layer_files, file_paths):
441
+ layer_files.pop(_bad, None)
442
+ # A pattern must show its defining trait here too, or the headline
443
+ # contradicts the architecture prose that already enforces this.
444
+ required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
445
+ if required and not required.issubset(layer_files.keys()):
446
+ continue
447
+ score = len(layer_files)
428
448
  if score > best_score:
429
449
  best_score = score
430
450
  best_pattern = pattern_name
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes