sourcecode 2.5.5__tar.gz → 2.5.6__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.5 → sourcecode-2.5.6}/CHANGELOG.md +60 -0
  2. {sourcecode-2.5.5 → sourcecode-2.5.6}/PKG-INFO +1 -1
  3. {sourcecode-2.5.5 → sourcecode-2.5.6}/pyproject.toml +1 -1
  4. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/architecture_analyzer.py +46 -8
  6. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/reconciliation.py +114 -3
  7. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/summarizer.py +3 -1
  8. {sourcecode-2.5.5 → sourcecode-2.5.6}/.github/workflows/build-windows.yml +0 -0
  9. {sourcecode-2.5.5 → sourcecode-2.5.6}/.gitignore +0 -0
  10. {sourcecode-2.5.5 → sourcecode-2.5.6}/.ruff.toml +0 -0
  11. {sourcecode-2.5.5 → sourcecode-2.5.6}/CONTRIBUTING.md +0 -0
  12. {sourcecode-2.5.5 → sourcecode-2.5.6}/LICENSE +0 -0
  13. {sourcecode-2.5.5 → sourcecode-2.5.6}/README.md +0 -0
  14. {sourcecode-2.5.5 → sourcecode-2.5.6}/SECURITY.md +0 -0
  15. {sourcecode-2.5.5 → sourcecode-2.5.6}/raw +0 -0
  16. {sourcecode-2.5.5 → sourcecode-2.5.6}/scripts/compare_integration_engines.py +0 -0
  17. {sourcecode-2.5.5 → sourcecode-2.5.6}/scripts/customer_smoke_test.sh +0 -0
  18. {sourcecode-2.5.5 → sourcecode-2.5.6}/scripts/generate_jdk_exports.py +0 -0
  19. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/adaptive_scanner.py +0 -0
  20. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/archetype.py +0 -0
  21. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/architecture_summary.py +0 -0
  22. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/ast_extractor.py +0 -0
  23. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/cache.py +0 -0
  24. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/call_surface.py +0 -0
  25. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/caller_metrics.py +0 -0
  26. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/canonical_ir.py +0 -0
  27. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/cir_graphs.py +0 -0
  28. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/classifier.py +0 -0
  29. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/cli.py +0 -0
  30. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/code_notes_analyzer.py +0 -0
  31. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/confidence_analyzer.py +0 -0
  32. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/context_cache.py +0 -0
  33. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/context_graph.py +0 -0
  34. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/context_scorer.py +0 -0
  35. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/context_summarizer.py +0 -0
  36. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/contract_model.py +0 -0
  37. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/contract_pipeline.py +0 -0
  38. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/coverage_parser.py +0 -0
  39. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/dependency_analyzer.py +0 -0
  40. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/__init__.py +0 -0
  41. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/base.py +0 -0
  42. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/csproj_parser.py +0 -0
  43. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/dart.py +0 -0
  44. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/dotnet.py +0 -0
  45. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/elixir.py +0 -0
  46. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/go.py +0 -0
  47. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/heuristic.py +0 -0
  48. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/hybrid.py +0 -0
  49. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/java.py +0 -0
  50. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/jvm_ext.py +0 -0
  51. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/nodejs.py +0 -0
  52. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/parsers.py +0 -0
  53. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/php.py +0 -0
  54. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/project.py +0 -0
  55. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/python.py +0 -0
  56. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/ruby.py +0 -0
  57. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/rust.py +0 -0
  58. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/systems.py +0 -0
  59. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/terraform.py +0 -0
  60. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/detectors/tooling.py +0 -0
  61. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/doc_analyzer.py +0 -0
  62. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/dynamic_argument_surface.py +0 -0
  63. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/endpoint_literals.py +0 -0
  64. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/endpoint_metrics.py +0 -0
  65. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/entrypoint_classifier.py +0 -0
  66. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/env_analyzer.py +0 -0
  67. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/error_schema.py +0 -0
  68. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/evidence_provider.py +0 -0
  69. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/explain.py +0 -0
  70. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/file_chunker.py +0 -0
  71. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/file_classifier.py +0 -0
  72. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/format_contract.py +0 -0
  73. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/fqn_utils.py +0 -0
  74. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/git_analyzer.py +0 -0
  75. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/graph_analyzer.py +0 -0
  76. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/graph_evidence.py +0 -0
  77. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/hibernate_strat.py +0 -0
  78. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/jdk_exports.py +0 -0
  79. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/license.py +0 -0
  80. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/mcp/__init__.py +0 -0
  81. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  82. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  83. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  84. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  85. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  86. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/mcp/orchestrator.py +0 -0
  87. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/mcp/registry.py +0 -0
  88. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/mcp/runner.py +0 -0
  89. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/mcp/server.py +0 -0
  90. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/mcp_nudge.py +0 -0
  91. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/metrics_analyzer.py +0 -0
  92. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/migrate_check.py +0 -0
  93. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/openapi_surface.py +0 -0
  94. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/output_budget.py +0 -0
  95. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/path_filters.py +0 -0
  96. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/pr_comment_renderer.py +0 -0
  97. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/pr_impact.py +0 -0
  98. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/prepare_context.py +0 -0
  99. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/progress.py +0 -0
  100. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/ranking_engine.py +0 -0
  101. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/redactor.py +0 -0
  102. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/relevance_scorer.py +0 -0
  103. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/rename_refactor.py +0 -0
  104. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/repo_classifier.py +0 -0
  105. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/repository_ir.py +0 -0
  106. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/__init__.py +0 -0
  107. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/context.py +0 -0
  108. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/errors.py +0 -0
  109. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/executor.py +0 -0
  110. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/planner.py +0 -0
  111. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/query.py +0 -0
  112. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/request.py +0 -0
  113. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/resolution.py +0 -0
  114. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/result.py +0 -0
  115. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/retriever.py +0 -0
  116. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/runtime.py +0 -0
  117. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/steps.py +0 -0
  118. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  119. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/steps_graph.py +0 -0
  120. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/steps_impact.py +0 -0
  121. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/steps_intf.py +0 -0
  122. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/steps_struct.py +0 -0
  123. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  124. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/ris.py +0 -0
  125. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/runtime_classifier.py +0 -0
  126. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/scanner.py +0 -0
  127. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/schema.py +0 -0
  128. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/security_config.py +0 -0
  129. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/security_posture.py +0 -0
  130. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/semantic_analyzer.py +0 -0
  131. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/semantic_impact_engine.py +0 -0
  132. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/semantic_integration_engine.py +0 -0
  133. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/semantic_services.py +0 -0
  134. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/serializer.py +0 -0
  135. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/spring_event_topology.py +0 -0
  136. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/spring_findings.py +0 -0
  137. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/spring_impact.py +0 -0
  138. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/spring_model.py +0 -0
  139. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/spring_security_audit.py +0 -0
  140. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/spring_semantic.py +0 -0
  141. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/spring_tx_analyzer.py +0 -0
  142. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/telemetry/__init__.py +0 -0
  143. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/telemetry/config.py +0 -0
  144. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/telemetry/consent.py +0 -0
  145. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/telemetry/events.py +0 -0
  146. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/telemetry/filters.py +0 -0
  147. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/telemetry/transport.py +0 -0
  148. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/tree_utils.py +0 -0
  149. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/type_usage_surface.py +0 -0
  150. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/validation_inference.py +0 -0
  151. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/validation_surface.py +0 -0
  152. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/version_check.py +0 -0
  153. {sourcecode-2.5.5 → sourcecode-2.5.6}/src/sourcecode/workspace.py +0 -0
  154. {sourcecode-2.5.5 → sourcecode-2.5.6}/supabase/functions/README.md +0 -0
  155. {sourcecode-2.5.5 → sourcecode-2.5.6}/supabase/functions/get-license/index.ts +0 -0
  156. {sourcecode-2.5.5 → sourcecode-2.5.6}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  157. {sourcecode-2.5.5 → sourcecode-2.5.6}/supabase/functions/telemetry/index.ts +0 -0
  158. {sourcecode-2.5.5 → sourcecode-2.5.6}/supabase/sql/license_event_ordering.sql +0 -0
  159. {sourcecode-2.5.5 → sourcecode-2.5.6}/supabase/sql/licensing_schema.sql +0 -0
  160. {sourcecode-2.5.5 → sourcecode-2.5.6}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.6] — 2026-07-17
4
+
5
+ **A layer must be attested by code.** The architecture fallback chain reported
6
+ directories as architecture; measured on the field-test repos, every fabricated
7
+ claim rested on files that are not code.
8
+
9
+ ### Fixed
10
+
11
+ - **Build and asset directories were reported as modules (P1-D-REMAINING)**
12
+ (`reconciliation.py`, `architecture_analyzer.py`, `summarizer.py`):
13
+
14
+ | repo | claimed | what it rested on |
15
+ |---|---|---|
16
+ | ofbiz | *"modular: docker, framework, runtime, gradle"* | shell scripts, a wrapper jar, a `.gitignore` |
17
+ | neo4j | *"modular: packaging, annotations, community"* | `packaging/` is a build directory |
18
+ | eureka | *"modular: …, images, …"* | five PNGs |
19
+ | openmrs | *"modular: …, webapp, monitoring"* | GIFs and yaml |
20
+ | petclinic | *"Layered Architecture with orchestration, data layers"* | ONE directory entry and a `settings.gradle` — with 47 Java files visible and ignored |
21
+ | jobrunr | *"Fullstack with frontend, backend layers"* | a JVM library; the "frontend" is `dashboard/ui/model/`, **16 Java files** |
22
+
23
+ Two new Evidence Reconciliation rules, mirror images of each other and both
24
+ applied to the duplicated layer tables (architecture prose + headline):
25
+
26
+ - **`layer_code_attestation`** — a claimed MODULE or logical code layer that
27
+ holds no code of the repo's runtime is not one. Stricter sibling of
28
+ `layer_language_coherence`, which deliberately ignores runtime-less layers
29
+ so that a real Spring-MVC `templates/` view survives: a view may be
30
+ templates, a module may not.
31
+ - **`composition_runtime_split`** — a pattern that IS the split must show it.
32
+ `fullstack` names a frontend TIER and a backend TIER; if every layer runs on
33
+ one runtime, the layer names are a directory-name coincidence. Not applied
34
+ to `monorepo`/`microservices` — an all-TypeScript monorepo, or same-runtime
35
+ services, are perfectly real.
36
+
37
+ petclinic's stem-based layering is Python-shaped (`cli.py`, `*_analyzer.py`,
38
+ `config.py`) and matches almost nothing in Java, which is why two stray hits
39
+ could mint a whole "Layered Architecture".
40
+
41
+ - **Module names that never name a module** (`architecture_analyzer.py`): the
42
+ reverse-DNS package roots every JVM repo starts its namespace with
43
+ (`org`/`com`/`io`/`net` — jobrunr reported a module called *"org"*) and
44
+ multi-release source sets (`src/main/java17` minted *"java17"*; now as
45
+ transparent as `java/`). Same class as the `core` already excluded.
46
+
47
+ Validation across 10 field-test repos through the real CLI: ofbiz →
48
+ *"framework, applications"* (its two real modules); neo4j → *"annotations,
49
+ community"*; eureka → its eight real `eureka-*` modules; openmrs → *"web,
50
+ test-suite, api, liquibase"*; jobrunr → *"modular: jobrunr, language-support,
51
+ framework-support"* (its real top-level directories); **petclinic emits no
52
+ architecture claim at all** — the fabricated line is gone while every true
53
+ statement survives (*"Java server-side mvc web app using Spring Boot,
54
+ Thymeleaf"*, frameworks, bootstrap annotation). Broadleaf, alfresco, keycloak
55
+ and jenkins: byte-identical prose.
56
+
57
+ Investigated and refuted along the way: these claims are NOT built on a
58
+ truncated file list — the analyzer sees 100% of petclinic's Java and 99.4% of
59
+ ofbiz's.
60
+
61
+ 11 new tests; suite 3448 passed.
62
+
3
63
  ## [2.5.5] — 2026-07-16
4
64
 
5
65
  **An architecture claim must belong to one program.** Third Evidence
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.5
3
+ Version: 2.5.6
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.5"
7
+ version = "2.5.6"
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.5"
7
+ __version__ = "2.5.6"
@@ -4,7 +4,11 @@ import re
4
4
  from pathlib import Path
5
5
  from typing import Literal, Optional
6
6
 
7
- from sourcecode.reconciliation import incoherent_layers
7
+ from sourcecode.reconciliation import (
8
+ incoherent_layers,
9
+ pattern_contradicted,
10
+ unattested_layers,
11
+ )
8
12
 
9
13
  from sourcecode.schema import (
10
14
  ArchitectureAnalysis,
@@ -41,11 +45,27 @@ _SRC_TRANSPARENT = {
41
45
  "src", "lib", "app", "pkg",
42
46
  "main", "java", "kotlin", "scala", "groovy",
43
47
  }
48
+
49
+ # `src/main/java17/` is a MULTI-RELEASE source set, as transparent as `java/`
50
+ # — JobRunr's minted a module called "java17".
51
+ _VERSIONED_SRC_RE = re.compile(r"^(?:java|kotlin|scala)\d+$")
52
+
53
+
54
+ def _is_transparent_segment(name: str) -> bool:
55
+ """A path segment that carries no architectural meaning of its own."""
56
+ low = name.lower()
57
+ return low in _SRC_TRANSPARENT or bool(_VERSIONED_SRC_RE.match(low))
44
58
  _CODE_EXTENSIONS = {
45
59
  ".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs",
46
60
  ".go", ".java", ".kt", ".rs", ".rb",
47
61
  }
48
- _GENERIC_NAMES = {"utils", "helpers", "common", "shared", "misc", "core", "root", ""}
62
+ # Names that never identify a module: generic buckets, and the reverse-DNS
63
+ # package roots every JVM repo starts its namespace with (JobRunr reported a
64
+ # module called "org").
65
+ _GENERIC_NAMES = {
66
+ "utils", "helpers", "common", "shared", "misc", "core", "root", "",
67
+ "org", "com", "io", "net",
68
+ }
49
69
 
50
70
  _TEST_DIRS: frozenset[str] = frozenset({
51
71
  "tests", "test", "spec", "specs", "__tests__", "e2e",
@@ -671,14 +691,18 @@ class ArchitectureAnalyzer:
671
691
  # reconciliation rule drops layers owned by a different runtime;
672
692
  # template/asset-only layers are untouched, so a real Spring-MVC
673
693
  # `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
- ):
694
+ _layer_paths = {k: [source_paths[i] for i in v] for k, v in layer_files.items()}
695
+ for _bad in incoherent_layers(pattern_name, _layer_paths, source_paths):
679
696
  matched.pop(_bad, None)
680
697
  layer_files.pop(_bad, None)
681
698
 
699
+ # A claim that IS a runtime split must show one: JobRunr, a JVM
700
+ # library, read "Fullstack with frontend, backend layers" off a
701
+ # `dashboard/ui/model/` package of 16 Java files. Nothing to prune —
702
+ # the pattern itself is what the evidence contradicts.
703
+ if pattern_contradicted(pattern_name, _layer_paths, source_paths):
704
+ continue
705
+
682
706
  # A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
683
707
  required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
684
708
  if required and not required.issubset(matched.keys()):
@@ -842,6 +866,14 @@ class ArchitectureAnalyzer:
842
866
  k: v for k, v in layer_files.items()
843
867
  if v and (not total or len(v) / total >= _LAYER_MIN_SHARE)
844
868
  }
869
+ # P1-D residue: these stem patterns are Python-shaped (cli.py,
870
+ # *_analyzer.py, config.py). On a Java repo almost nothing matches, so
871
+ # the few stray hits are not a layering — petclinic's "Layered
872
+ # Architecture with orchestration, data layers" rested on ONE directory
873
+ # entry plus a settings.gradle, with 47 Java files visible and ignored.
874
+ # A logical code layer that holds no code of this repo is not one.
875
+ for _bad in unattested_layers(non_empty, paths):
876
+ non_empty.pop(_bad, None)
845
877
  if len(non_empty) >= 2:
846
878
  return "layered", [
847
879
  ArchitectureLayer(name=k, pattern="layered", files=v, confidence="low")
@@ -861,13 +893,19 @@ class ArchitectureAnalyzer:
861
893
  for p in paths:
862
894
  parts = p.replace("\\", "/").split("/")
863
895
  for part in parts[:-1]:
864
- if (part not in _SRC_TRANSPARENT
896
+ if (not _is_transparent_segment(part)
865
897
  and part.lower() not in _NON_SOURCE_DIRS
866
898
  and part.lower() not in _GENERIC_NAMES):
867
899
  module_files.setdefault(part, []).append(p)
868
900
  break
869
901
 
870
902
  meaningful = {k: v for k, v in module_files.items() if len(v) >= 3}
903
+ # P1-D residue: a MODULE of this repo must hold this repo's code. File
904
+ # count alone let build and asset directories be reported as modules —
905
+ # ofbiz "docker, framework, runtime, gradle", neo4j "packaging",
906
+ # eureka "images" (five PNGs). The real modules survive untouched.
907
+ for _bad in unattested_layers(meaningful, paths):
908
+ meaningful.pop(_bad, None)
871
909
  if len(meaningful) >= 2:
872
910
  return "modular", [
873
911
  ArchitectureLayer(name=k, pattern="modular", files=v, confidence="low")
@@ -223,6 +223,15 @@ _CROSS_RUNTIME_PATTERNS: frozenset[str] = frozenset({
223
223
  "fullstack", "microservices", "monorepo",
224
224
  })
225
225
 
226
+ # Patterns whose meaning IS the runtime split — a "fullstack" repo names a
227
+ # frontend TIER and a backend TIER. If every layer runs on the same runtime
228
+ # there is no split, and the claim is a directory-name coincidence: JobRunr, a
229
+ # JVM library, read "Fullstack with frontend, backend layers" off a
230
+ # `dashboard/ui/model/` package holding 16 JAVA files. (Not monorepo or
231
+ # microservices: an all-TypeScript monorepo, or same-runtime services, are
232
+ # perfectly real.)
233
+ _SPLIT_REQUIRED_PATTERNS: frozenset[str] = frozenset({"fullstack"})
234
+
226
235
 
227
236
  def runtime_census(layer_files: dict[str, list[str]]) -> dict[str, dict[str, int]]:
228
237
  """{layer -> {runtime_family -> file count}} — the language evidence view.
@@ -288,6 +297,91 @@ def _rule_layer_language_coherence(
288
297
  ]
289
298
 
290
299
 
300
+ def _rule_layer_code_attestation(
301
+ census: dict[str, dict[str, int]],
302
+ repo_runtimes: dict[str, int],
303
+ ) -> list[ReconciliationFinding]:
304
+ """A claimed code module holds no code of the repo's runtime.
305
+
306
+ Stricter sibling of `layer_language_coherence`, for claims whose layers are
307
+ modules or logical code layers — their whole role is to hold code, so a
308
+ directory with none is not one of them. (`layer_language_coherence` cannot
309
+ serve here: it deliberately ignores layers with no runtime files at all, so
310
+ that a real Spring-MVC `templates/` view survives. A view may be templates;
311
+ a MODULE may not.)
312
+
313
+ Measured on the field-test repos: ofbiz's "modular architecture" listed
314
+ `docker`, `gradle`, `runtime` and `themes` (shell scripts, wrapper jars, a
315
+ .gitignore, images — zero Java); neo4j listed `packaging`; eureka listed
316
+ `images` (five PNGs); petclinic's whole "Layered Architecture with
317
+ orchestration, data layers" rested on one directory entry and a
318
+ settings.gradle.
319
+ """
320
+ if not repo_runtimes:
321
+ return []
322
+ dominant = max(sorted(repo_runtimes), key=lambda f: repo_runtimes[f])
323
+ unattested = sorted(
324
+ layer for layer, counts in census.items() if not counts.get(dominant)
325
+ )
326
+ if not unattested:
327
+ return []
328
+ return [
329
+ ReconciliationFinding(
330
+ rule="layer_code_attestation",
331
+ symbols=tuple(unattested),
332
+ detail=(
333
+ f"{len(unattested)} claimed layer(s) hold no {dominant} code — "
334
+ "a directory without the repo's code is not a module of it."
335
+ ),
336
+ )
337
+ ]
338
+
339
+
340
+ def _rule_composition_runtime_split(
341
+ pattern: str,
342
+ census: dict[str, dict[str, int]],
343
+ ) -> list[ReconciliationFinding]:
344
+ """A pattern that names a runtime split shows none.
345
+
346
+ The mirror image of `layer_language_coherence`: a claim about one program
347
+ must NOT span runtimes; a claim that IS the split must show it. Silent
348
+ unless the pattern's layers carry runtime evidence at all.
349
+ """
350
+ if pattern not in _SPLIT_REQUIRED_PATTERNS or len(census) < 2:
351
+ return []
352
+ families = {fam for counts in census.values() for fam in counts}
353
+ if len(families) != 1:
354
+ return [] # a real split, or no runtime evidence to judge
355
+ only = next(iter(families))
356
+ return [
357
+ ReconciliationFinding(
358
+ rule="composition_runtime_split",
359
+ symbols=tuple(sorted(census)),
360
+ detail=(
361
+ f"the '{pattern}' claim names a runtime split but every layer is "
362
+ f"{only} — the layer names are a coincidence, not two tiers."
363
+ ),
364
+ )
365
+ ]
366
+
367
+
368
+ def unattested_layers(
369
+ layer_files: dict[str, list[str]], repo_files: "list[str]"
370
+ ) -> frozenset[str]:
371
+ """Layers holding no code of the repo's runtime (convenience view).
372
+
373
+ For consumers claiming code modules / logical code layers. Never raises.
374
+ """
375
+ try:
376
+ repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
377
+ out: set[str] = set()
378
+ for f in _rule_layer_code_attestation(runtime_census(layer_files), repo_runtimes):
379
+ out.update(f.symbols)
380
+ return frozenset(out)
381
+ except Exception:
382
+ return frozenset()
383
+
384
+
291
385
  def reconcile_architecture_evidence(
292
386
  *,
293
387
  pattern: str,
@@ -304,17 +398,34 @@ def reconcile_architecture_evidence(
304
398
  """
305
399
  findings: list[ReconciliationFinding] = []
306
400
  try:
401
+ census = runtime_census(layer_files)
307
402
  repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
308
403
  findings.extend(
309
- _rule_layer_language_coherence(
310
- pattern, runtime_census(layer_files), repo_runtimes
311
- )
404
+ _rule_layer_language_coherence(pattern, census, repo_runtimes)
312
405
  )
406
+ findings.extend(_rule_composition_runtime_split(pattern, census))
313
407
  except Exception:
314
408
  pass
315
409
  return findings
316
410
 
317
411
 
412
+ def pattern_contradicted(
413
+ pattern: str, layer_files: dict[str, list[str]], repo_files: "list[str]"
414
+ ) -> bool:
415
+ """True when a rule rejects the PATTERN itself, not just some of its layers.
416
+
417
+ `incoherent_layers` prunes layers and lets the pattern re-qualify on what
418
+ is left; a split-less "fullstack" has nothing to re-qualify with — the
419
+ claim as a whole is the thing contradicted.
420
+ """
421
+ return any(
422
+ f.rule == "composition_runtime_split"
423
+ for f in reconcile_architecture_evidence(
424
+ pattern=pattern, layer_files=layer_files, repo_files=repo_files
425
+ )
426
+ )
427
+
428
+
318
429
  def incoherent_layers(
319
430
  pattern: str, layer_files: dict[str, list[str]], repo_files: "list[str]"
320
431
  ) -> frozenset[str]:
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  from typing import Any
10
10
 
11
11
  from sourcecode.architecture_analyzer import _LAYER_MIN_SHARE, _PATTERN_REQUIRED_KEYS
12
- from sourcecode.reconciliation import incoherent_layers
12
+ from sourcecode.reconciliation import incoherent_layers, pattern_contradicted
13
13
  from sourcecode.detectors.parsers import load_json_file, load_toml_file
14
14
  from sourcecode.tree_utils import safe_read_text
15
15
  from sourcecode.entrypoint_classifier import is_production_entry_point
@@ -439,6 +439,8 @@ class ProjectSummarizer:
439
439
  # two duplicated tables leaves the other one lying.
440
440
  for _bad in incoherent_layers(pattern_name, layer_files, file_paths):
441
441
  layer_files.pop(_bad, None)
442
+ if pattern_contradicted(pattern_name, layer_files, file_paths):
443
+ continue
442
444
  # A pattern must show its defining trait here too, or the headline
443
445
  # contradicts the architecture prose that already enforces this.
444
446
  required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes