sourcecode 2.5.13__tar.gz → 2.5.16__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 (162) hide show
  1. {sourcecode-2.5.13 → sourcecode-2.5.16}/CHANGELOG.md +151 -7
  2. {sourcecode-2.5.13 → sourcecode-2.5.16}/PKG-INFO +1 -1
  3. {sourcecode-2.5.13 → sourcecode-2.5.16}/pyproject.toml +1 -1
  4. sourcecode-2.5.16/scripts/perf_harness.py +252 -0
  5. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/ast_extractor.py +12 -11
  7. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/canonical_ir.py +8 -0
  8. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/classifier.py +80 -52
  9. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/cli.py +18 -0
  10. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/context_graph.py +35 -0
  11. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/java.py +35 -1
  12. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/endpoint_metrics.py +4 -1
  13. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/path_filters.py +31 -0
  14. sourcecode-2.5.16/src/sourcecode/perf.py +268 -0
  15. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/repository_ir.py +39 -1
  16. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/serializer.py +35 -2
  17. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/summarizer.py +7 -17
  18. {sourcecode-2.5.13 → sourcecode-2.5.16}/.github/workflows/build-windows.yml +0 -0
  19. {sourcecode-2.5.13 → sourcecode-2.5.16}/.gitignore +0 -0
  20. {sourcecode-2.5.13 → sourcecode-2.5.16}/.ruff.toml +0 -0
  21. {sourcecode-2.5.13 → sourcecode-2.5.16}/CONTRIBUTING.md +0 -0
  22. {sourcecode-2.5.13 → sourcecode-2.5.16}/LICENSE +0 -0
  23. {sourcecode-2.5.13 → sourcecode-2.5.16}/README.md +0 -0
  24. {sourcecode-2.5.13 → sourcecode-2.5.16}/SECURITY.md +0 -0
  25. {sourcecode-2.5.13 → sourcecode-2.5.16}/raw +0 -0
  26. {sourcecode-2.5.13 → sourcecode-2.5.16}/scripts/compare_integration_engines.py +0 -0
  27. {sourcecode-2.5.13 → sourcecode-2.5.16}/scripts/customer_smoke_test.sh +0 -0
  28. {sourcecode-2.5.13 → sourcecode-2.5.16}/scripts/generate_jdk_exports.py +0 -0
  29. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/adaptive_scanner.py +0 -0
  30. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/archetype.py +0 -0
  31. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/architecture_analyzer.py +0 -0
  32. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/architecture_summary.py +0 -0
  33. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/cache.py +0 -0
  34. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/call_surface.py +0 -0
  35. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/caller_metrics.py +0 -0
  36. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/cir_graphs.py +0 -0
  37. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/code_notes_analyzer.py +0 -0
  38. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/confidence_analyzer.py +0 -0
  39. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/context_cache.py +0 -0
  40. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/context_scorer.py +0 -0
  41. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/context_summarizer.py +0 -0
  42. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/contract_model.py +0 -0
  43. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/contract_pipeline.py +0 -0
  44. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/coverage_parser.py +0 -0
  45. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/dependency_analyzer.py +0 -0
  46. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/__init__.py +0 -0
  47. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/base.py +0 -0
  48. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/csproj_parser.py +0 -0
  49. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/dart.py +0 -0
  50. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/dotnet.py +0 -0
  51. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/elixir.py +0 -0
  52. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/go.py +0 -0
  53. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/heuristic.py +0 -0
  54. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/hybrid.py +0 -0
  55. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/jvm_ext.py +0 -0
  56. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/nodejs.py +0 -0
  57. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/parsers.py +0 -0
  58. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/php.py +0 -0
  59. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/project.py +0 -0
  60. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/python.py +0 -0
  61. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/ruby.py +0 -0
  62. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/rust.py +0 -0
  63. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/systems.py +0 -0
  64. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/terraform.py +0 -0
  65. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/detectors/tooling.py +0 -0
  66. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/doc_analyzer.py +0 -0
  67. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/dynamic_argument_surface.py +0 -0
  68. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/endpoint_literals.py +0 -0
  69. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/entrypoint_classifier.py +0 -0
  70. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/env_analyzer.py +0 -0
  71. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/error_schema.py +0 -0
  72. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/evidence_provider.py +0 -0
  73. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/explain.py +0 -0
  74. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/file_chunker.py +0 -0
  75. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/file_classifier.py +0 -0
  76. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/format_contract.py +0 -0
  77. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/fqn_utils.py +0 -0
  78. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/git_analyzer.py +0 -0
  79. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/graph_analyzer.py +0 -0
  80. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/graph_evidence.py +0 -0
  81. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/hibernate_strat.py +0 -0
  82. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/jdk_exports.py +0 -0
  83. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/license.py +0 -0
  84. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/mcp/__init__.py +0 -0
  85. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  86. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  87. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  88. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  89. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  90. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/mcp/orchestrator.py +0 -0
  91. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/mcp/registry.py +0 -0
  92. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/mcp/runner.py +0 -0
  93. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/mcp/server.py +0 -0
  94. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/mcp_nudge.py +0 -0
  95. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/metrics_analyzer.py +0 -0
  96. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/migrate_check.py +0 -0
  97. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/openapi_surface.py +0 -0
  98. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/output_budget.py +0 -0
  99. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/pr_comment_renderer.py +0 -0
  100. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/pr_impact.py +0 -0
  101. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/prepare_context.py +0 -0
  102. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/progress.py +0 -0
  103. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/ranking_engine.py +0 -0
  104. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/reconciliation.py +0 -0
  105. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/redactor.py +0 -0
  106. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/relevance_scorer.py +0 -0
  107. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/rename_refactor.py +0 -0
  108. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/repo_classifier.py +0 -0
  109. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/__init__.py +0 -0
  110. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/context.py +0 -0
  111. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/errors.py +0 -0
  112. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/executor.py +0 -0
  113. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/planner.py +0 -0
  114. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/query.py +0 -0
  115. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/request.py +0 -0
  116. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/resolution.py +0 -0
  117. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/result.py +0 -0
  118. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/retriever.py +0 -0
  119. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/runtime.py +0 -0
  120. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/steps.py +0 -0
  121. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  122. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/steps_graph.py +0 -0
  123. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/steps_impact.py +0 -0
  124. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/steps_intf.py +0 -0
  125. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/steps_struct.py +0 -0
  126. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  127. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/ris.py +0 -0
  128. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/runtime_classifier.py +0 -0
  129. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/scanner.py +0 -0
  130. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/schema.py +0 -0
  131. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/security_config.py +0 -0
  132. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/security_posture.py +0 -0
  133. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/semantic_analyzer.py +0 -0
  134. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/semantic_impact_engine.py +0 -0
  135. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/semantic_integration_engine.py +0 -0
  136. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/semantic_services.py +0 -0
  137. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/spring_event_topology.py +0 -0
  138. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/spring_findings.py +0 -0
  139. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/spring_impact.py +0 -0
  140. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/spring_model.py +0 -0
  141. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/spring_security_audit.py +0 -0
  142. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/spring_semantic.py +0 -0
  143. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/spring_tx_analyzer.py +0 -0
  144. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/telemetry/__init__.py +0 -0
  145. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/telemetry/config.py +0 -0
  146. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/telemetry/consent.py +0 -0
  147. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/telemetry/events.py +0 -0
  148. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/telemetry/filters.py +0 -0
  149. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/telemetry/transport.py +0 -0
  150. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/tree_utils.py +0 -0
  151. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/type_usage_surface.py +0 -0
  152. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/validation_inference.py +0 -0
  153. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/validation_surface.py +0 -0
  154. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/version_check.py +0 -0
  155. {sourcecode-2.5.13 → sourcecode-2.5.16}/src/sourcecode/workspace.py +0 -0
  156. {sourcecode-2.5.13 → sourcecode-2.5.16}/supabase/functions/README.md +0 -0
  157. {sourcecode-2.5.13 → sourcecode-2.5.16}/supabase/functions/get-license/index.ts +0 -0
  158. {sourcecode-2.5.13 → sourcecode-2.5.16}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  159. {sourcecode-2.5.13 → sourcecode-2.5.16}/supabase/functions/telemetry/index.ts +0 -0
  160. {sourcecode-2.5.13 → sourcecode-2.5.16}/supabase/sql/license_event_ordering.sql +0 -0
  161. {sourcecode-2.5.13 → sourcecode-2.5.16}/supabase/sql/licensing_schema.sql +0 -0
  162. {sourcecode-2.5.13 → sourcecode-2.5.16}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,14 +1,127 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.16] — 2026-07-19
4
+
5
+ **Performance campaign close-out: one real algorithmic win, cost-per-symbol
6
+ measurable, one candidate refuted by measurement.** The ADR-0006 harness localized
7
+ cold wall to the `analysis` phase (90–96% on every non-trivial repo); profiling
8
+ inside it found — and this release fixes — the one structural bottleneck, populates
9
+ the last empty baseline axes, and records a negative result that stopped a
10
+ non-winning change from landing.
11
+
12
+ ### Performance
13
+
14
+ - **Killed an O(files × total_symbols) scan in per-file Java contract projection**
15
+ (`ast_extractor.py`, `context_graph.py`): `_java_contract_from_graph` ran once per
16
+ Java file and called the whole-repo `graph.symbols()`/`relations()` unfiltered, then
17
+ kept only its own file's rows. Added lazy once-built groupings
18
+ `ContextGraph.symbols_in_file(path)` / `relations_from(fqn)` (O(1) after build); the
19
+ projection now walks only edges sourced at its own types. Behavior-equivalent
20
+ (normalized old-vs-new on petclinic + keycloak; 1430 IR/spring/endpoint/impact tests
21
+ green). Cold wall p50, same host, speedup tracking file count as the O(files × N)
22
+ model predicts: neo4j 33.6→16.7s (2.0×), broadleaf 27.9→15.4s (1.8×),
23
+ keycloak 20.8→14.1s (1.5×), openmrs 7.9→5.8s (1.4×), ofbiz 19.4→15.0s (1.3×).
24
+
25
+ ### Added
26
+
27
+ - **`repo.classes` / `repo.methods` populated in the perf baseline** from repo-ir's
28
+ symbol taxonomy (`repository_ir.py`, `scripts/perf_harness.py`): `analysis_meta` now
29
+ emits a deterministic `symbol_kinds` breakdown plus two rollups —
30
+ `classes` = class+interface+enum+annotation+record, `methods` = method+constructor —
31
+ and the harness reads them via one `repo-ir --summary-only` pass off the measured path
32
+ (never in `wall_ms`; repo-ir failure falls back to 0, an honest "unmeasured"). Closes
33
+ the last empty axes in the `perf-baseline-v1` schema, enabling cost-per-symbol.
34
+
35
+ ### Notes
36
+
37
+ - **A body-fact-scanning optimization was investigated and reverted after measurement
38
+ showed no win.** cProfile flagged per-method body re-slicing (`_method_body` 44508× on
39
+ ofbiz) as a lead; a byte-identical single-pass rewrite (0 mismatches / 5555 files)
40
+ measured 7674→7721 ms — no improvement. The profiler had flagged a call-count-heavy but
41
+ wall-cheap region; the real cost is inherent regex/char scanning, already past its
42
+ algorithmic win. The change was dropped rather than accrue complexity on a hot,
43
+ output-critical path for no gain.
44
+
45
+ ## [2.5.15] — 2026-07-19
46
+
47
+ **Bootstrap detection recovers the real entry point on multi-`main` repos, without
48
+ noise.** Post-F1 field validation (neo4j) found the compact `bootstrap` was name-gated
49
+ to `*Application.java` / `*Main.java`: neo4j's real server entry `Neo4jCommunity.java`
50
+ (a `main()` in a differently-named class) was invisible, so a client CLI `Main.java` was
51
+ reported as the sole bootstrap and the repo's 31 `main()` methods were hidden.
52
+
53
+ ### Fixed
54
+
55
+ - **`main()`-bearing classes are recovered regardless of filename, comment-aware, and
56
+ the bootstrap list is ordered/capped/counted** (`detectors/java.py`, `serializer.py`,
57
+ `path_filters.py`): the annotation scan (which already reads each candidate file) now
58
+ also emits an `application` entry when a file declares a real `main(String[])` or a
59
+ bootstrap annotation — after stripping comments, so a `main()` / `@SpringBootApplication`
60
+ shown in a **javadoc example** is never mistaken for an entry (fixed Broadleaf
61
+ `*AutoConfiguration` false positives). `_bootstrap_structured` then: excludes entries
62
+ classified `auxiliary`/`development`; excludes mains in **example/demo modules**
63
+ (new structural `is_example_module_path`, e.g. `eureka-examples/`) and **test-fixture
64
+ modules** (`is_test_fixture_module_path`, e.g. `eureka-test-utils/`) — filtered here,
65
+ not in the detector, because workspace sub-scans prefix the module name onto the path
66
+ only after detection; orders deployment-annotated entries first; caps the list at 10;
67
+ and adds a `bootstrap_note` reporting the true count when more than one entry exists.
68
+ Measured (fleet): neo4j surfaces `Neo4jCommunity.java` with note "11 application entry
69
+ points detected; showing 10"; Broadleaf false positives 7 → 0; eureka's example/
70
+ test-util mains dropped (5 → 1); petclinic / spaghetti / openmrs unchanged (single
71
+ bootstrap). Full-output A/B confirms the change is confined to the entry-point surface —
72
+ no drift in stacks, architecture, or confidence. Residual real tool/CLI mains in library
73
+ packages (e.g. jobrunr's `DatabaseCreator`) are reported honestly with the count, as they
74
+ cannot be distinguished from deployment mains without name heuristics. Suite: 3521 passed.
75
+
76
+ ## [2.5.14] — 2026-07-18
77
+
78
+ **One authority decides the primary stack; every surface transports its choice.**
79
+ Post-F1 field validation found a polyglot repo (Confluent `examples`: 1 C# file vs 86
80
+ Java files) labelled `.NET` as primary. Root cause was not one coin flip but several:
81
+ `primary` was chosen by `max()` over `(priority, confidence, manifest)` — which **ties**
82
+ for two manifest-detected API stacks, so **list order** decided — and then re-derived
83
+ independently in more than one place (the classifier marked `stacks[].primary`; the
84
+ summarizer re-selected for the `project_summary` prose; the serializer / architecture
85
+ summary invented a `stacks[0]` fallback when the flag was absent). These agreed only by
86
+ sharing the same arbitrary order, so they could — and did — disagree once the input
87
+ changed.
88
+
89
+ ### Fixed
90
+
91
+ - **Single primary-stack authority with an evidence-weighted tiebreaker**
92
+ (`classifier.py`, `cli.py`, `summarizer.py`): a new `select_primary_stack(stacks,
93
+ project_type, file_paths)` is the ONE decider. It keeps the existing
94
+ `(priority, confidence, manifest)` ordering and adds **source-file volume as the last
95
+ tuple element** — a pure tiebreaker that decides only when the prior three tie, so a
96
+ 1-file .NET example never outranks 86 Java files, and stacks that already differ are
97
+ never reordered. It marks exactly one instance `primary=True` (all others `False`) over
98
+ the final assembled root+workspace set. `TypeClassifier.enrich` now delegates to it (the
99
+ old `_select_primary_stack` name-returning helper is removed), and the main compact path
100
+ re-runs it after workspace stacks are appended. The summarizer's independent
101
+ `_select_summary_primary_stack` is deleted — the prose now reads the same `stack.primary`
102
+ flag as `architecture_summary`, `stacks[].primary` and `confidence_analyzer`. The
103
+ extension table shared by confidence enrichment and the tiebreaker was hoisted to one
104
+ constant (`_STACK_EXTENSIONS`) so the two never diverge. Fleet A/B (12 repos): only
105
+ `examples` changes (primary `dotnet`→`java`; architecture `C#/.NET`→`Java … JAX-RS`;
106
+ summary `Stack: Dotnet`→`Stack: Java … 80 Java classes`; the previously-emitted false
107
+ "polyglot / two programs" anomaly correctly disappears because Java genuinely attests the
108
+ API surface). All 11 other repos are byte-identical. Suite: 3508 passed, 5 skipped.
109
+
3
110
  ## [2.5.13] — 2026-07-18
4
111
 
5
- **Test-fixture endpoints no longer pollute the production API surface.** Post-F1 field
6
- validation (neo4j, keycloak) found that a JAX-RS / Spring resource shipping under
7
- `src/main` of a test-utility module — a dummy web service, an Arquillian test-harness
8
- resource — was reported flat as production API surface with no scope qualifier. This
9
- inflated endpoint counts and injected phantom attack surface into security exploration,
10
- the exact task the endpoint command is meant to serve, with no warning. This is the one
11
- defect the post-F1 review classified as blocking.
112
+ **"Production endpoint" is now a single definition across every user-facing surface.**
113
+ Post-F1 field validation (neo4j, keycloak) found that a JAX-RS / Spring resource shipping
114
+ under `src/main` of a test-utility module — a dummy web service, an Arquillian test-harness
115
+ resource — was reported as production API surface, and that different commands disagreed on
116
+ what counted: `endpoints` / `validation` derived their view one way while `impact`,
117
+ `impact-chain`, `pr-impact`, `review-pr` and `spring-audit` derived theirs from the raw
118
+ `route_surface`, so the same repo had two definitions of "exposed endpoint" depending on the
119
+ command. This release unifies them: every user-facing surface now consumes the same
120
+ production-endpoint view (test scaffolding excluded), while `route_surface` remains the RAW
121
+ structural evidence by design — full and unfiltered — consumed only by `repo-ir`. The
122
+ under-modelling that motivated this (test fixtures counted as HTTP exposure, inflating
123
+ endpoint counts, blast radius, risk scores and security analysis) was the one defect the
124
+ post-F1 review classified as blocking.
12
125
 
13
126
  ### Fixed
14
127
 
@@ -32,6 +145,37 @@ defect the post-F1 review classified as blocking.
32
145
  deliberately untouched — the new helper is narrower and wired only into endpoint
33
146
  extraction.
34
147
 
148
+ - **One production-endpoint definition across every CIR consumer** (`repository_ir.py`,
149
+ `canonical_ir.py`, `endpoint_metrics.py`): the fix above only reached the `endpoints` /
150
+ `validation` surface; `impact-chain`, `pr-impact`, `review-pr` and `spring-audit` derive
151
+ their HTTP surface from the canonical `cir.endpoints`, which is built from the RAW
152
+ `route_surface` and did **not** inherit the test-fixture exclusion — so those commands
153
+ still counted test scaffolding as production exposure (e.g. keycloak's 102 Arquillian
154
+ endpoints inflating blast radius / risk score / SEC analysis). `build_repo_ir` now tags
155
+ test-fixture routes with `scope="test_util"` on `route_surface`, and `ir_dict_to_canonical`
156
+ excludes tagged routes when building `cir.endpoints` — the same place the pre-existing
157
+ FQN dynamic-admin filter already reconciles the CIR surface with the `endpoints` command.
158
+ `route_surface` itself is left **raw and complete** (the tag is additive) so `repo-ir`
159
+ keeps full evidence; the divergence is documented in `ENDPOINT_SURFACE_RECONCILIATION`.
160
+ No heuristic, scoring, risk or reconciliation logic was changed — the four consumers change
161
+ only because they now receive a coherent `cir.endpoints`. Measured: keycloak
162
+ `cir.endpoints` / `spring-audit.endpoints_analyzed` 775 → 673 (route_surface stays 775);
163
+ neo4j 18 → 14 (route_surface stays 18); Broadleaf/petclinic/spaghetti/eureka unchanged
164
+ (0 test_util routes). Zero test-fixture controllers leak into `cir.endpoints` on any repo.
165
+
166
+ - **The legacy `impact` command now shares the same production-endpoint definition**
167
+ (`repository_ir.py`): `impact` reads `route_surface` directly via `compute_blast_radius`
168
+ rather than through `cir.endpoints`, so it was the last user surface still counting test
169
+ fixtures as HTTP exposure. `compute_blast_radius` now skips `scope=="test_util"` routes when
170
+ building `endpoints_affected`. The change is a single guard at the consumption point: the
171
+ blast-radius algorithm, caller selection, risk formula, security surface and the FQN-admin
172
+ handling are all unchanged — the endpoint list simply no longer includes test scaffolding.
173
+ Validated by A/B (stash) diff: `impact TestingOIDCEndpointsApplicationResource` on keycloak
174
+ went from 28 endpoints_affected (all test) / risk 87 to 0 / risk 3 with `direct_callers`
175
+ unchanged (1 → 1); `impact CatalogServiceImpl` on Broadleaf is **byte-identical** before and
176
+ after (54 endpoints, 5 FQN-shaped paths preserved, 30 callers). "Production endpoint" is now
177
+ a single definition across every user-facing command.
178
+
35
179
  ## [2.5.12] — 2026-07-18
36
180
 
37
181
  **F-1 (the Evidence Reconciliation Layer) is closed and its rule core frozen.** This release
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.13
3
+ Version: 2.5.16
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.13"
7
+ version = "2.5.16"
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"
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env python3
2
+ """Performance harness — runs the reference fleet under the ADR-0006 contract.
3
+
4
+ For every (command x repo x mode) cell it runs the CLI N times, records
5
+ wall-clock, peak RSS and (when the in-process recorder is armed) per-phase
6
+ timings, then writes one ``perf-baseline-v1`` JSON artifact.
7
+
8
+ Methodology is fixed by ADR-0006 §5: cold and warm are separate cells and never
9
+ averaged; the first warm run is discarded as cache fill; central tendency is the
10
+ median; p95 is always reported; cross-machine numbers are recorded, never diffed.
11
+
12
+ Usage:
13
+ python3 scripts/perf_harness.py --quick # petclinic only, 2 runs
14
+ python3 scripts/perf_harness.py # full fleet, 7 runs
15
+ python3 scripts/perf_harness.py --repos keycloak eureka --runs 5
16
+ python3 scripts/perf_harness.py --dry-run # print the plan, run nothing
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import resource
25
+ import shutil
26
+ import subprocess
27
+ import sys
28
+ import tempfile
29
+ import time
30
+ from pathlib import Path
31
+
32
+ # Import the frozen collector from the package under test.
33
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
34
+ from sourcecode import __version__ as TOOL_VERSION # noqa: E402
35
+ from sourcecode import perf # noqa: E402
36
+
37
+ # ── Fleet definition ────────────────────────────────────────────────────────
38
+ # Directory name under the fleet root. Repos are pinned by their checked-out
39
+ # commit at run time (recorded in each artifact), so a size change is never
40
+ # mistaken for a perf change.
41
+ DEFAULT_FLEET_ROOT = Path(
42
+ os.environ.get("PERF_FLEET_ROOT", "/Users/user/Documents/workspace/testing")
43
+ )
44
+ FLEET: dict[str, str] = {
45
+ "petclinic": "spring-petclinic",
46
+ "broadleaf": "BroadleafCommerce",
47
+ "openmrs": "openmrs-core",
48
+ "keycloak": "keycloak",
49
+ "eureka": "eureka",
50
+ "jobrunr": "jobrunr",
51
+ "ofbiz": "ofbiz-framework",
52
+ "alfresco": "alfresco-community-repo",
53
+ "jenkins": "jenkins",
54
+ "neo4j": "neo4j",
55
+ }
56
+
57
+ # Commands to profile. The empty string is the root analysis (`ask <repo>`);
58
+ # named entries are subcommands (`ask <cmd> <repo>`).
59
+ DEFAULT_COMMANDS = [""]
60
+
61
+ ASK_BIN = os.environ.get("ASK_BIN", "ask")
62
+ BYTES_PER_KB = 1024
63
+ # darwin ru_maxrss is bytes; linux reports kilobytes.
64
+ RSS_UNIT = 1 if sys.platform == "darwin" else BYTES_PER_KB
65
+
66
+
67
+ def git_commit(repo_path: Path) -> str:
68
+ try:
69
+ out = subprocess.run(
70
+ ["git", "-C", str(repo_path), "rev-parse", "--short", "HEAD"],
71
+ capture_output=True, text=True, timeout=15,
72
+ )
73
+ return out.stdout.strip() or "unknown"
74
+ except (subprocess.SubprocessError, OSError):
75
+ return "unknown"
76
+
77
+
78
+ def repo_size(repo_path: Path) -> dict[str, int]:
79
+ """Repo scaling axes for cost-per-unit analysis.
80
+
81
+ ``files`` / ``bytes`` are a cheap rglob over every ``*.java`` (the frozen file
82
+ axis — includes tests). ``classes`` / ``methods`` come from repo-ir's
83
+ ``analysis_meta`` rollup (the IR owns the symbol taxonomy). This is one
84
+ ``repo-ir --summary-only`` pass off the measured path; it never touches
85
+ ``wall_ms``. If repo-ir fails the symbol counts fall back to 0 — an honest
86
+ "unmeasured", never a fabricated number.
87
+ """
88
+ files = 0
89
+ total = 0
90
+ for p in repo_path.rglob("*.java"):
91
+ try:
92
+ total += p.stat().st_size
93
+ files += 1
94
+ except OSError:
95
+ continue
96
+ classes, methods = _repo_symbol_counts(repo_path)
97
+ return {"files": files, "bytes": total, "classes": classes, "methods": methods}
98
+
99
+
100
+ def _repo_symbol_counts(repo_path: Path) -> tuple[int, int]:
101
+ """(classes, methods) from ``repo-ir --summary-only``; (0, 0) on any failure."""
102
+ with tempfile.TemporaryDirectory(prefix="perf-ir-") as td:
103
+ out = Path(td) / "ir.json"
104
+ try:
105
+ subprocess.run(
106
+ [ASK_BIN, "repo-ir", str(repo_path), "--summary-only", "-o", str(out)],
107
+ capture_output=True, timeout=1800, check=False,
108
+ )
109
+ meta = json.loads(out.read_text(encoding="utf-8")).get("analysis_meta", {})
110
+ return int(meta.get("classes", 0) or 0), int(meta.get("methods", 0) or 0)
111
+ except (subprocess.SubprocessError, OSError, ValueError):
112
+ return 0, 0
113
+
114
+
115
+ def build_argv(command: str, repo_path: Path, cold: bool) -> list[str]:
116
+ argv = [ASK_BIN]
117
+ if command:
118
+ argv.append(command)
119
+ argv.append(str(repo_path))
120
+ argv += ["--format", "json"]
121
+ if cold:
122
+ argv.append("--no-cache")
123
+ return argv
124
+
125
+
126
+ def run_once(argv: list[str], env: dict[str, str], perf_log: Path) -> tuple[float, float, dict | None]:
127
+ """Run the CLI once. Returns (wall_ms, rss_bytes, phase_ms | None)."""
128
+ if perf_log.exists():
129
+ perf_log.unlink()
130
+ rss_before = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss
131
+ t0 = time.perf_counter()
132
+ subprocess.run(argv, env=env, capture_output=True, timeout=1800)
133
+ wall_ms = (time.perf_counter() - t0) * 1000.0
134
+ rss_after = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss
135
+ # ru_maxrss for RUSAGE_CHILDREN is the peak among reaped children; the delta
136
+ # approximates this run's peak. Approximate by construction — documented.
137
+ rss_bytes = max(rss_after - rss_before, rss_after) * RSS_UNIT if rss_after else 0.0
138
+ phase_ms = _read_phase_log(perf_log)
139
+ return wall_ms, float(rss_bytes), phase_ms
140
+
141
+
142
+ def _read_phase_log(perf_log: Path) -> dict | None:
143
+ if not perf_log.exists():
144
+ return None
145
+ try:
146
+ last = perf_log.read_text(encoding="utf-8").strip().splitlines()[-1]
147
+ return json.loads(last).get("phase_ms") or None
148
+ except (OSError, IndexError, ValueError):
149
+ return None
150
+
151
+
152
+ def measure_cell(command: str, repo_path: Path, mode: str, runs: int) -> dict:
153
+ cold = mode == "cold"
154
+ walls: list[float] = []
155
+ rsss: list[float] = []
156
+ phases: list[dict] = []
157
+
158
+ with tempfile.TemporaryDirectory(prefix="perf-cache-") as cache_dir, \
159
+ tempfile.TemporaryDirectory(prefix="perf-log-") as log_dir:
160
+ perf_log = Path(log_dir) / "phases.jsonl"
161
+ base_env = dict(os.environ)
162
+ base_env[perf.ENV_ENABLE] = "1"
163
+ base_env[perf.ENV_LOG] = str(perf_log)
164
+ base_env["SOURCECODE_CACHE_DIR"] = cache_dir
165
+
166
+ # Warm mode: one un-recorded priming run to populate the cache, then the
167
+ # first measured warm run is still discarded (belt and suspenders).
168
+ n = runs + 1 if mode == "warm" else runs
169
+ for i in range(n):
170
+ argv = build_argv(command, repo_path, cold=cold)
171
+ if cold:
172
+ # Each cold run gets a pristine cache dir.
173
+ for child in Path(cache_dir).iterdir():
174
+ shutil.rmtree(child, ignore_errors=True) if child.is_dir() else child.unlink()
175
+ wall, rss, phase = run_once(argv, base_env, perf_log)
176
+ if mode == "warm" and i == 0:
177
+ continue # discard cache-fill run
178
+ walls.append(wall)
179
+ rsss.append(rss)
180
+ if phase:
181
+ phases.append(phase)
182
+
183
+ return perf.build_baseline_cell(
184
+ tool_version=TOOL_VERSION,
185
+ command=command or "ask",
186
+ repo={"name": repo_path.name, "commit": git_commit(repo_path), **repo_size(repo_path)},
187
+ mode=mode,
188
+ wall_ms_runs=walls,
189
+ rss_bytes_runs=rsss,
190
+ phase_ms_runs=phases or None,
191
+ )
192
+
193
+
194
+ def main() -> int:
195
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
196
+ ap.add_argument("--repos", nargs="*", help="Subset of fleet names (default: all).")
197
+ ap.add_argument("--commands", nargs="*", default=DEFAULT_COMMANDS,
198
+ help="Commands to profile; '' = root analysis (default).")
199
+ ap.add_argument("--modes", nargs="*", default=["cold", "warm"], choices=["cold", "warm"])
200
+ ap.add_argument("--runs", type=int, default=7, help="Measured runs per cell (>=5 per ADR-0006).")
201
+ ap.add_argument("--fleet-root", type=Path, default=DEFAULT_FLEET_ROOT)
202
+ ap.add_argument("--out", type=Path, default=None,
203
+ help="Output dir (default: docs/perf/baselines/<tool_version>).")
204
+ ap.add_argument("--quick", action="store_true", help="petclinic only, 2 runs — smoke test.")
205
+ ap.add_argument("--dry-run", action="store_true", help="Print the plan, run nothing.")
206
+ args = ap.parse_args()
207
+
208
+ if args.quick:
209
+ args.repos = ["petclinic"]
210
+ args.runs = 2
211
+
212
+ repos = args.repos or list(FLEET)
213
+ out_dir = args.out or (Path(__file__).resolve().parent.parent / "docs" / "perf" / "baselines" / TOOL_VERSION)
214
+
215
+ plan = [
216
+ (cmd, name, mode)
217
+ for cmd in args.commands
218
+ for name in repos
219
+ for mode in args.modes
220
+ ]
221
+ print(f"perf-harness {TOOL_VERSION} — {len(plan)} cells, {args.runs} runs each")
222
+ print(f"fleet root: {args.fleet_root}")
223
+ print(f"output: {out_dir}")
224
+ if args.dry_run:
225
+ for cmd, name, mode in plan:
226
+ print(f" [{mode:4}] {name:10} {cmd or 'ask'}")
227
+ return 0
228
+
229
+ out_dir.mkdir(parents=True, exist_ok=True)
230
+ for cmd, name, mode in plan:
231
+ subdir = FLEET.get(name, name)
232
+ repo_path = args.fleet_root / subdir
233
+ if not repo_path.is_dir():
234
+ print(f" SKIP {name}: {repo_path} not found", file=sys.stderr)
235
+ continue
236
+ print(f" [{mode:4}] {name:10} {cmd or 'ask'} ...", end="", flush=True)
237
+ try:
238
+ cell = measure_cell(cmd, repo_path, mode, args.runs)
239
+ except subprocess.TimeoutExpired:
240
+ print(" TIMEOUT", file=sys.stderr)
241
+ continue
242
+ fname = f"{name}__{cmd or 'ask'}__{mode}.json"
243
+ (out_dir / fname).write_text(json.dumps(cell, indent=2) + "\n", encoding="utf-8")
244
+ w = cell["wall_ms"]
245
+ print(f" p50={w['p50']}ms p95={w['p95']}ms -> {fname}")
246
+
247
+ print(f"done. artifacts in {out_dir}")
248
+ return 0
249
+
250
+
251
+ if __name__ == "__main__":
252
+ raise SystemExit(main())
@@ -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.13"
7
+ __version__ = "2.5.16"
@@ -967,25 +967,26 @@ def _java_contract_from_graph(rel_path: str, graph: "Any") -> FileContract:
967
967
  nodes/edges instead of re-parsing source. Method signatures are the graph's
968
968
  canonical compact form (`(Type)->Return`); annotations are node-grounded.
969
969
  """
970
- file_syms = [s for s in graph.symbols() if s.source_file == rel_path]
970
+ file_syms = graph.symbols_in_file(rel_path)
971
971
  type_syms = [s for s in file_syms if s.kind in ("class", "interface", "enum", "annotation")]
972
972
  type_fqns = {s.fqn for s in type_syms}
973
973
 
974
974
  # extends / implements edges out of this file's types (targets are simple
975
975
  # names for out-of-repo supertypes, resolved FQNs for in-repo ones — the
976
- # same forms the old regex captured for the unresolved case).
976
+ # same forms the old regex captured for the unresolved case). Only edges
977
+ # sourced at this file's types matter, so index straight to them instead of
978
+ # scanning the whole-repo relation set once per file.
977
979
  extends_of: dict[str, list[str]] = {}
978
980
  implements_of: dict[str, list[str]] = {}
979
981
  import_targets: set[str] = set()
980
- for r in graph.relations():
981
- if r.source not in type_fqns:
982
- continue
983
- if r.kind == "extends":
984
- extends_of.setdefault(r.source, []).append(r.target)
985
- elif r.kind == "implements":
986
- implements_of.setdefault(r.source, []).append(r.target)
987
- elif r.kind == "imports":
988
- import_targets.add(r.target)
982
+ for fqn in type_fqns:
983
+ for r in graph.relations_from(fqn):
984
+ if r.kind == "extends":
985
+ extends_of.setdefault(r.source, []).append(r.target)
986
+ elif r.kind == "implements":
987
+ implements_of.setdefault(r.source, []).append(r.target)
988
+ elif r.kind == "imports":
989
+ import_targets.add(r.target)
989
990
 
990
991
  exports: list[ExportRecord] = []
991
992
  types: list[TypeDefinition] = []
@@ -398,9 +398,17 @@ def ir_dict_to_canonical(
398
398
  # real REST surface. The CIR endpoint list feeds `spring-audit` (endpoints_analyzed),
399
399
  # `impact` and `validation`; it must apply the SAME filter so its count reconciles
400
400
  # with `endpoints` instead of being silently inflated by those pseudo-routes.
401
+ # Endpoints declared in a test-fixture / test-utility module are tagged
402
+ # scope="test_util" on route_surface by build_repo_ir. They are excluded from the
403
+ # canonical endpoint list for the same reason as the FQN filter above: the CIR
404
+ # surface feeds impact-chain, pr-impact, review-pr and spring-audit, and test
405
+ # scaffolding is not production HTTP exposure. route_surface itself keeps them
406
+ # (raw evidence); only this canonical projection drops them.
401
407
  _seen_ids: set[str] = set()
402
408
  raw_endpoints: list[CanonicalEndpoint] = []
403
409
  for r in route_surface:
410
+ if r.get("scope") == "test_util":
411
+ continue
404
412
  if _FQN_PATH_RE.search(r.get("path", "") or ""):
405
413
  continue
406
414
  ep = _route_to_canonical_endpoint(r)