sourcecode 2.5.12__tar.gz → 2.5.15__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.
- {sourcecode-2.5.12 → sourcecode-2.5.15}/CHANGELOG.md +134 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/PKG-INFO +1 -1
- {sourcecode-2.5.12 → sourcecode-2.5.15}/pyproject.toml +1 -1
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/__init__.py +1 -1
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/canonical_ir.py +8 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/classifier.py +80 -52
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/cli.py +7 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/java.py +35 -1
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/endpoint_metrics.py +4 -1
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/path_filters.py +79 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/repository_ir.py +59 -2
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/serializer.py +35 -2
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/summarizer.py +7 -17
- {sourcecode-2.5.12 → sourcecode-2.5.15}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/.gitignore +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/.ruff.toml +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/CONTRIBUTING.md +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/LICENSE +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/README.md +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/SECURITY.md +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/raw +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/archetype.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/explain.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/license.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/functions/README.md +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.5.12 → sourcecode-2.5.15}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,139 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.5.15] — 2026-07-19
|
|
4
|
+
|
|
5
|
+
**Bootstrap detection recovers the real entry point on multi-`main` repos, without
|
|
6
|
+
noise.** Post-F1 field validation (neo4j) found the compact `bootstrap` was name-gated
|
|
7
|
+
to `*Application.java` / `*Main.java`: neo4j's real server entry `Neo4jCommunity.java`
|
|
8
|
+
(a `main()` in a differently-named class) was invisible, so a client CLI `Main.java` was
|
|
9
|
+
reported as the sole bootstrap and the repo's 31 `main()` methods were hidden.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`main()`-bearing classes are recovered regardless of filename, comment-aware, and
|
|
14
|
+
the bootstrap list is ordered/capped/counted** (`detectors/java.py`, `serializer.py`,
|
|
15
|
+
`path_filters.py`): the annotation scan (which already reads each candidate file) now
|
|
16
|
+
also emits an `application` entry when a file declares a real `main(String[])` or a
|
|
17
|
+
bootstrap annotation — after stripping comments, so a `main()` / `@SpringBootApplication`
|
|
18
|
+
shown in a **javadoc example** is never mistaken for an entry (fixed Broadleaf
|
|
19
|
+
`*AutoConfiguration` false positives). `_bootstrap_structured` then: excludes entries
|
|
20
|
+
classified `auxiliary`/`development`; excludes mains in **example/demo modules**
|
|
21
|
+
(new structural `is_example_module_path`, e.g. `eureka-examples/`) and **test-fixture
|
|
22
|
+
modules** (`is_test_fixture_module_path`, e.g. `eureka-test-utils/`) — filtered here,
|
|
23
|
+
not in the detector, because workspace sub-scans prefix the module name onto the path
|
|
24
|
+
only after detection; orders deployment-annotated entries first; caps the list at 10;
|
|
25
|
+
and adds a `bootstrap_note` reporting the true count when more than one entry exists.
|
|
26
|
+
Measured (fleet): neo4j surfaces `Neo4jCommunity.java` with note "11 application entry
|
|
27
|
+
points detected; showing 10"; Broadleaf false positives 7 → 0; eureka's example/
|
|
28
|
+
test-util mains dropped (5 → 1); petclinic / spaghetti / openmrs unchanged (single
|
|
29
|
+
bootstrap). Full-output A/B confirms the change is confined to the entry-point surface —
|
|
30
|
+
no drift in stacks, architecture, or confidence. Residual real tool/CLI mains in library
|
|
31
|
+
packages (e.g. jobrunr's `DatabaseCreator`) are reported honestly with the count, as they
|
|
32
|
+
cannot be distinguished from deployment mains without name heuristics. Suite: 3521 passed.
|
|
33
|
+
|
|
34
|
+
## [2.5.14] — 2026-07-18
|
|
35
|
+
|
|
36
|
+
**One authority decides the primary stack; every surface transports its choice.**
|
|
37
|
+
Post-F1 field validation found a polyglot repo (Confluent `examples`: 1 C# file vs 86
|
|
38
|
+
Java files) labelled `.NET` as primary. Root cause was not one coin flip but several:
|
|
39
|
+
`primary` was chosen by `max()` over `(priority, confidence, manifest)` — which **ties**
|
|
40
|
+
for two manifest-detected API stacks, so **list order** decided — and then re-derived
|
|
41
|
+
independently in more than one place (the classifier marked `stacks[].primary`; the
|
|
42
|
+
summarizer re-selected for the `project_summary` prose; the serializer / architecture
|
|
43
|
+
summary invented a `stacks[0]` fallback when the flag was absent). These agreed only by
|
|
44
|
+
sharing the same arbitrary order, so they could — and did — disagree once the input
|
|
45
|
+
changed.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **Single primary-stack authority with an evidence-weighted tiebreaker**
|
|
50
|
+
(`classifier.py`, `cli.py`, `summarizer.py`): a new `select_primary_stack(stacks,
|
|
51
|
+
project_type, file_paths)` is the ONE decider. It keeps the existing
|
|
52
|
+
`(priority, confidence, manifest)` ordering and adds **source-file volume as the last
|
|
53
|
+
tuple element** — a pure tiebreaker that decides only when the prior three tie, so a
|
|
54
|
+
1-file .NET example never outranks 86 Java files, and stacks that already differ are
|
|
55
|
+
never reordered. It marks exactly one instance `primary=True` (all others `False`) over
|
|
56
|
+
the final assembled root+workspace set. `TypeClassifier.enrich` now delegates to it (the
|
|
57
|
+
old `_select_primary_stack` name-returning helper is removed), and the main compact path
|
|
58
|
+
re-runs it after workspace stacks are appended. The summarizer's independent
|
|
59
|
+
`_select_summary_primary_stack` is deleted — the prose now reads the same `stack.primary`
|
|
60
|
+
flag as `architecture_summary`, `stacks[].primary` and `confidence_analyzer`. The
|
|
61
|
+
extension table shared by confidence enrichment and the tiebreaker was hoisted to one
|
|
62
|
+
constant (`_STACK_EXTENSIONS`) so the two never diverge. Fleet A/B (12 repos): only
|
|
63
|
+
`examples` changes (primary `dotnet`→`java`; architecture `C#/.NET`→`Java … JAX-RS`;
|
|
64
|
+
summary `Stack: Dotnet`→`Stack: Java … 80 Java classes`; the previously-emitted false
|
|
65
|
+
"polyglot / two programs" anomaly correctly disappears because Java genuinely attests the
|
|
66
|
+
API surface). All 11 other repos are byte-identical. Suite: 3508 passed, 5 skipped.
|
|
67
|
+
|
|
68
|
+
## [2.5.13] — 2026-07-18
|
|
69
|
+
|
|
70
|
+
**"Production endpoint" is now a single definition across every user-facing surface.**
|
|
71
|
+
Post-F1 field validation (neo4j, keycloak) found that a JAX-RS / Spring resource shipping
|
|
72
|
+
under `src/main` of a test-utility module — a dummy web service, an Arquillian test-harness
|
|
73
|
+
resource — was reported as production API surface, and that different commands disagreed on
|
|
74
|
+
what counted: `endpoints` / `validation` derived their view one way while `impact`,
|
|
75
|
+
`impact-chain`, `pr-impact`, `review-pr` and `spring-audit` derived theirs from the raw
|
|
76
|
+
`route_surface`, so the same repo had two definitions of "exposed endpoint" depending on the
|
|
77
|
+
command. This release unifies them: every user-facing surface now consumes the same
|
|
78
|
+
production-endpoint view (test scaffolding excluded), while `route_surface` remains the RAW
|
|
79
|
+
structural evidence by design — full and unfiltered — consumed only by `repo-ir`. The
|
|
80
|
+
under-modelling that motivated this (test fixtures counted as HTTP exposure, inflating
|
|
81
|
+
endpoint counts, blast radius, risk scores and security analysis) was the one defect the
|
|
82
|
+
post-F1 review classified as blocking.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- **Endpoints from test-fixture / test-utility modules are excluded from the production
|
|
87
|
+
surface** (`repository_ir.py`, `path_filters.py`): a new structural, name-agnostic
|
|
88
|
+
helper `is_test_fixture_module_path` flags a module directory segment that carries a
|
|
89
|
+
delimited `test` token *together* with an infrastructure word (`util(s)`, `fixture(s)`,
|
|
90
|
+
`support`, `framework`, `kit`, `harness`, `tools`, `helper(s)`, `mock(s)`, `stub(s)`,
|
|
91
|
+
`dummy`) — e.g. `server-test-utils`, `foo-testkit`, `bar-test-support` — or a known
|
|
92
|
+
test-module segment (`testsuite`, `integration-arquillian`, `test-framework`). Requiring
|
|
93
|
+
both tokens in the *same* segment keeps legitimate `utils` / `support` / `tools` packages
|
|
94
|
+
from ever matching. `extract_java_endpoints` traces each endpoint back to its declaring
|
|
95
|
+
module and, when it is a test fixture, drops it from `endpoints` / `total` /
|
|
96
|
+
`no_security_signal` and the security-model heuristics. The exclusion is **transparent,
|
|
97
|
+
not silent**: excluded entries are preserved in a `test_util_endpoints` bucket with a
|
|
98
|
+
`scope: "test_util"` tag, a `test_util_excluded` count, and an explanatory `notes` entry.
|
|
99
|
+
Measured: neo4j 18 → 14 production (4 dummy `DummyThirdPartyWebService` endpoints moved to
|
|
100
|
+
the bucket); keycloak 775 → 673 production (102 Arquillian/test-framework harness
|
|
101
|
+
endpoints excluded). Control repos unchanged (spaghetti-api 7, petclinic 17, eureka 18,
|
|
102
|
+
Broadleaf 225). `is_test_or_fixture_path` and the migrate-check blocking count it feeds are
|
|
103
|
+
deliberately untouched — the new helper is narrower and wired only into endpoint
|
|
104
|
+
extraction.
|
|
105
|
+
|
|
106
|
+
- **One production-endpoint definition across every CIR consumer** (`repository_ir.py`,
|
|
107
|
+
`canonical_ir.py`, `endpoint_metrics.py`): the fix above only reached the `endpoints` /
|
|
108
|
+
`validation` surface; `impact-chain`, `pr-impact`, `review-pr` and `spring-audit` derive
|
|
109
|
+
their HTTP surface from the canonical `cir.endpoints`, which is built from the RAW
|
|
110
|
+
`route_surface` and did **not** inherit the test-fixture exclusion — so those commands
|
|
111
|
+
still counted test scaffolding as production exposure (e.g. keycloak's 102 Arquillian
|
|
112
|
+
endpoints inflating blast radius / risk score / SEC analysis). `build_repo_ir` now tags
|
|
113
|
+
test-fixture routes with `scope="test_util"` on `route_surface`, and `ir_dict_to_canonical`
|
|
114
|
+
excludes tagged routes when building `cir.endpoints` — the same place the pre-existing
|
|
115
|
+
FQN dynamic-admin filter already reconciles the CIR surface with the `endpoints` command.
|
|
116
|
+
`route_surface` itself is left **raw and complete** (the tag is additive) so `repo-ir`
|
|
117
|
+
keeps full evidence; the divergence is documented in `ENDPOINT_SURFACE_RECONCILIATION`.
|
|
118
|
+
No heuristic, scoring, risk or reconciliation logic was changed — the four consumers change
|
|
119
|
+
only because they now receive a coherent `cir.endpoints`. Measured: keycloak
|
|
120
|
+
`cir.endpoints` / `spring-audit.endpoints_analyzed` 775 → 673 (route_surface stays 775);
|
|
121
|
+
neo4j 18 → 14 (route_surface stays 18); Broadleaf/petclinic/spaghetti/eureka unchanged
|
|
122
|
+
(0 test_util routes). Zero test-fixture controllers leak into `cir.endpoints` on any repo.
|
|
123
|
+
|
|
124
|
+
- **The legacy `impact` command now shares the same production-endpoint definition**
|
|
125
|
+
(`repository_ir.py`): `impact` reads `route_surface` directly via `compute_blast_radius`
|
|
126
|
+
rather than through `cir.endpoints`, so it was the last user surface still counting test
|
|
127
|
+
fixtures as HTTP exposure. `compute_blast_radius` now skips `scope=="test_util"` routes when
|
|
128
|
+
building `endpoints_affected`. The change is a single guard at the consumption point: the
|
|
129
|
+
blast-radius algorithm, caller selection, risk formula, security surface and the FQN-admin
|
|
130
|
+
handling are all unchanged — the endpoint list simply no longer includes test scaffolding.
|
|
131
|
+
Validated by A/B (stash) diff: `impact TestingOIDCEndpointsApplicationResource` on keycloak
|
|
132
|
+
went from 28 endpoints_affected (all test) / risk 87 to 0 / risk 3 with `direct_callers`
|
|
133
|
+
unchanged (1 → 1); `impact CatalogServiceImpl` on Broadleaf is **byte-identical** before and
|
|
134
|
+
after (54 endpoints, 5 FQN-shaped paths preserved, 30 callers). "Production endpoint" is now
|
|
135
|
+
a single definition across every user-facing command.
|
|
136
|
+
|
|
3
137
|
## [2.5.12] — 2026-07-18
|
|
4
138
|
|
|
5
139
|
**F-1 (the Evidence Reconciliation Layer) is closed and its rule core frozen.** This release
|
|
@@ -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)
|
|
@@ -37,6 +37,26 @@ _WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter", "Phoe
|
|
|
37
37
|
_CLI_FRAMEWORKS = {"Typer", "Cobra", "Clap"}
|
|
38
38
|
_API_STACKS = {"python", "go", "java", "php", "ruby", "dotnet", "kotlin", "scala"}
|
|
39
39
|
|
|
40
|
+
# Source-file extensions per stack — single table shared by the confidence
|
|
41
|
+
# enrichment (_count_extension_hits) and the primary-stack evidence tiebreaker
|
|
42
|
+
# (select_primary_stack). Kept in one place so the two never diverge.
|
|
43
|
+
_STACK_EXTENSIONS: dict[str, tuple[str, ...]] = {
|
|
44
|
+
"python": (".py",),
|
|
45
|
+
"nodejs": (".js", ".ts", ".tsx"),
|
|
46
|
+
"go": (".go",),
|
|
47
|
+
"rust": (".rs",),
|
|
48
|
+
"java": (".java",),
|
|
49
|
+
"php": (".php",),
|
|
50
|
+
"ruby": (".rb",),
|
|
51
|
+
"dart": (".dart",),
|
|
52
|
+
"dotnet": (".cs", ".fs"),
|
|
53
|
+
"elixir": (".ex", ".exs"),
|
|
54
|
+
"kotlin": (".kt", ".kts"),
|
|
55
|
+
"scala": (".scala",),
|
|
56
|
+
"terraform": (".tf",),
|
|
57
|
+
"cpp": (".c", ".cc", ".cpp", ".cxx", ".hpp", ".h"),
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
# Center-of-gravity gate for the "api" decision (neo4j field test).
|
|
41
61
|
# The historical rule was pure PRESENCE: any API framework on the classpath →
|
|
42
62
|
# project_type="api". That mislabels a large engine/platform that merely exposes a
|
|
@@ -63,6 +83,59 @@ _API_SURFACE_ENTRY_KINDS = frozenset({
|
|
|
63
83
|
ConfidenceLevel = Literal["high", "medium", "low"]
|
|
64
84
|
|
|
65
85
|
|
|
86
|
+
def _primary_ext_hits(stack_name: str, file_paths: Sequence[str]) -> int:
|
|
87
|
+
exts = _STACK_EXTENSIONS.get(stack_name, ())
|
|
88
|
+
if not exts:
|
|
89
|
+
return 0
|
|
90
|
+
return sum(1 for p in file_paths if p.endswith(exts))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def select_primary_stack(
|
|
94
|
+
stacks: Sequence[StackDetection],
|
|
95
|
+
project_type: str | None,
|
|
96
|
+
file_paths: Sequence[str] | None = None,
|
|
97
|
+
) -> list[StackDetection]:
|
|
98
|
+
"""Single authority for which stack is primary.
|
|
99
|
+
|
|
100
|
+
Marks exactly ONE stack instance ``primary=True`` (all others ``False``) on the
|
|
101
|
+
FINAL assembled stack set (root + workspace stacks). Selection order:
|
|
102
|
+
``(priority, confidence, manifest, source-file volume)``. The volume component is
|
|
103
|
+
a pure tiebreaker — it decides only when the prior three tie (e.g. a polyglot repo
|
|
104
|
+
where a Java and a .NET stack are both manifest-detected API stacks), so a 1-file
|
|
105
|
+
.NET example never outranks 86 Java files. Being last, it never reorders stacks
|
|
106
|
+
that already differ on the earlier components.
|
|
107
|
+
|
|
108
|
+
This is the ONLY place a primary decision is made. Every consumer must read
|
|
109
|
+
``stack.primary`` — none may re-select or invent a ``stacks[0]`` fallback.
|
|
110
|
+
"""
|
|
111
|
+
stack_list = list(stacks)
|
|
112
|
+
if not stack_list:
|
|
113
|
+
return stack_list
|
|
114
|
+
paths = list(file_paths or [])
|
|
115
|
+
|
|
116
|
+
def sort_key(stack: StackDetection) -> tuple[int, int, int, int]:
|
|
117
|
+
score = {"low": 0, "medium": 1, "high": 2}.get(stack.confidence, 0)
|
|
118
|
+
manifest_weight = 1 if stack.manifests else 0
|
|
119
|
+
priority = 0
|
|
120
|
+
# Backend server-side stacks with manifest evidence outrank frontend stacks
|
|
121
|
+
# in fullstack projects (e.g. Java+Spring wins over nodejs+Angular).
|
|
122
|
+
if (project_type in {"fullstack", "api"}
|
|
123
|
+
and stack.stack in _API_STACKS
|
|
124
|
+
and stack.manifests):
|
|
125
|
+
priority = 4
|
|
126
|
+
elif (project_type in {"webapp", "fullstack"} and stack.stack in {"nodejs", "elixir"}
|
|
127
|
+
or project_type == "api" and stack.stack in _API_STACKS
|
|
128
|
+
or project_type == "cli" and any(
|
|
129
|
+
framework.name in _CLI_FRAMEWORKS for framework in stack.frameworks
|
|
130
|
+
)
|
|
131
|
+
or project_type == "cli" and stack.stack in {"cpp", "dotnet"}):
|
|
132
|
+
priority = 3
|
|
133
|
+
return (priority, score, manifest_weight, _primary_ext_hits(stack.stack, paths))
|
|
134
|
+
|
|
135
|
+
winner = max(stack_list, key=sort_key)
|
|
136
|
+
return [replace(s, primary=(s is winner)) for s in stack_list]
|
|
137
|
+
|
|
138
|
+
|
|
66
139
|
class TypeClassifier:
|
|
67
140
|
"""Clasifica project_type y enriquece stacks con confianza/primary."""
|
|
68
141
|
|
|
@@ -74,15 +147,12 @@ class TypeClassifier:
|
|
|
74
147
|
) -> tuple[list[StackDetection], str | None]:
|
|
75
148
|
enriched = [self._enrich_stack(file_tree, stack, entry_points) for stack in stacks]
|
|
76
149
|
project_type = self._classify_project_type(file_tree, enriched, entry_points)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if is_primary:
|
|
84
|
-
primary_assigned = True
|
|
85
|
-
final_stacks.append(replace(stack, primary=is_primary))
|
|
150
|
+
# Delegate to the single primary-stack authority. enrich only ever sees the
|
|
151
|
+
# ROOT stacks; the authority runs again at model finalization over the full
|
|
152
|
+
# root+workspace set (that call is the one consumers read).
|
|
153
|
+
final_stacks = select_primary_stack(
|
|
154
|
+
enriched, project_type, flatten_file_tree(file_tree)
|
|
155
|
+
)
|
|
86
156
|
return final_stacks, project_type
|
|
87
157
|
|
|
88
158
|
def _enrich_stack(
|
|
@@ -371,50 +441,8 @@ class TypeClassifier:
|
|
|
371
441
|
has_api = True
|
|
372
442
|
return has_web and has_api
|
|
373
443
|
|
|
374
|
-
def _select_primary_stack(
|
|
375
|
-
self, stacks: Sequence[StackDetection], project_type: str | None
|
|
376
|
-
) -> str | None:
|
|
377
|
-
if not stacks:
|
|
378
|
-
return None
|
|
379
|
-
|
|
380
|
-
def sort_key(stack: StackDetection) -> tuple[int, int, int]:
|
|
381
|
-
score = {"low": 0, "medium": 1, "high": 2}.get(stack.confidence, 0)
|
|
382
|
-
manifest_weight = 1 if stack.manifests else 0
|
|
383
|
-
priority = 0
|
|
384
|
-
# Backend server-side stacks with manifest evidence outrank frontend stacks
|
|
385
|
-
# in fullstack projects (e.g. Java+Spring wins over nodejs+Angular).
|
|
386
|
-
if (project_type in {"fullstack", "api"}
|
|
387
|
-
and stack.stack in _API_STACKS
|
|
388
|
-
and stack.manifests):
|
|
389
|
-
priority = 4
|
|
390
|
-
elif (project_type in {"webapp", "fullstack"} and stack.stack in {"nodejs", "elixir"}
|
|
391
|
-
or project_type == "api" and stack.stack in _API_STACKS
|
|
392
|
-
or project_type == "cli" and any(
|
|
393
|
-
framework.name in _CLI_FRAMEWORKS for framework in stack.frameworks
|
|
394
|
-
)
|
|
395
|
-
or project_type == "cli" and stack.stack in {"cpp", "dotnet"}):
|
|
396
|
-
priority = 3
|
|
397
|
-
return (priority, score, manifest_weight)
|
|
398
|
-
|
|
399
|
-
return max(stacks, key=sort_key).stack
|
|
400
|
-
|
|
401
444
|
def _count_extension_hits(self, file_tree: dict[str, Any], stack: str) -> int:
|
|
402
|
-
extensions =
|
|
403
|
-
"python": (".py",),
|
|
404
|
-
"nodejs": (".js", ".ts", ".tsx"),
|
|
405
|
-
"go": (".go",),
|
|
406
|
-
"rust": (".rs",),
|
|
407
|
-
"java": (".java",),
|
|
408
|
-
"php": (".php",),
|
|
409
|
-
"ruby": (".rb",),
|
|
410
|
-
"dart": (".dart",),
|
|
411
|
-
"dotnet": (".cs", ".fs"),
|
|
412
|
-
"elixir": (".ex", ".exs"),
|
|
413
|
-
"kotlin": (".kt", ".kts"),
|
|
414
|
-
"scala": (".scala",),
|
|
415
|
-
"terraform": (".tf",),
|
|
416
|
-
"cpp": (".c", ".cc", ".cpp", ".cxx", ".hpp", ".h"),
|
|
417
|
-
}.get(stack, ())
|
|
445
|
+
extensions = _STACK_EXTENSIONS.get(stack, ())
|
|
418
446
|
return sum(1 for path in flatten_file_tree(file_tree) if path.endswith(extensions))
|
|
419
447
|
|
|
420
448
|
def _score_to_confidence(self, score: int) -> ConfidenceLevel:
|
|
@@ -1965,6 +1965,13 @@ def main(
|
|
|
1965
1965
|
analyzer_fingerprints=_fingerprints,
|
|
1966
1966
|
traversal_topology=_topology.as_dict(),
|
|
1967
1967
|
)
|
|
1968
|
+
# Single primary-stack authority over the FINAL assembled set (root + workspace
|
|
1969
|
+
# stacks). enrich marked the root stacks; workspace stacks were appended after,
|
|
1970
|
+
# so re-run the one authority here — idempotent — so every consumer of sm.stacks
|
|
1971
|
+
# reads a coherent primary flag.
|
|
1972
|
+
from sourcecode.classifier import select_primary_stack as _select_primary_stack
|
|
1973
|
+
from sourcecode.tree_utils import flatten_file_tree as _flatten_ft
|
|
1974
|
+
stacks = _select_primary_stack(stacks, project_type, _flatten_ft(file_tree))
|
|
1968
1975
|
sm = SourceMap(
|
|
1969
1976
|
metadata=metadata,
|
|
1970
1977
|
file_tree=file_tree,
|
|
@@ -67,6 +67,16 @@ _BOOTSTRAP_ANNOTATION_RE = re.compile(
|
|
|
67
67
|
r'EnableAutoConfiguration|SpringBootConfiguration)\b'
|
|
68
68
|
r'|\bextends\s+SpringBootServletInitializer\b'
|
|
69
69
|
)
|
|
70
|
+
# Comment strippers — a main()/bootstrap annotation inside a javadoc example or a
|
|
71
|
+
# comment is NOT an entry point (e.g. Broadleaf's *AutoConfiguration classes whose
|
|
72
|
+
# javadoc shows `public static void main` and mentions @SpringBootApplication).
|
|
73
|
+
# Detection of bootstrap entries must run on de-commented source.
|
|
74
|
+
_BLOCK_COMMENT_RE = re.compile(r'/\*.*?\*/', re.DOTALL)
|
|
75
|
+
_LINE_COMMENT_RE = re.compile(r'//[^\n]*')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _strip_java_comments(source: str) -> str:
|
|
79
|
+
return _LINE_COMMENT_RE.sub(' ', _BLOCK_COMMENT_RE.sub(' ', source))
|
|
70
80
|
|
|
71
81
|
# --- Keycloak / Quarkus SPI ---
|
|
72
82
|
# Matches "implements EventListenerProvider" or "implements Foo, EventListenerProvider, Bar"
|
|
@@ -533,12 +543,21 @@ class JavaDetector(AbstractDetector):
|
|
|
533
543
|
_other_files = [p for p in _non_test if "Controller" not in p]
|
|
534
544
|
scan_candidates = _ctrl_files + _other_files[:max(0, _MAX_JAVA_ENTRY_SCAN - len(_ctrl_files))]
|
|
535
545
|
|
|
546
|
+
# Paths already emitted as application entries by the name-gated step-1 scan,
|
|
547
|
+
# so the annotation scan (which now also recovers main()-bearing files) does
|
|
548
|
+
# not list the same bootstrap twice.
|
|
549
|
+
_app_paths = {ep.path for ep in entry_points if ep.kind == "application"}
|
|
536
550
|
annotation_eps: list[EntryPoint] = []
|
|
537
551
|
for rel_path in scan_candidates:
|
|
538
552
|
if len(annotation_eps) >= _MAX_ANNOTATION_ENTRY_POINTS:
|
|
539
553
|
break
|
|
540
554
|
abs_path = context.root / rel_path
|
|
541
|
-
|
|
555
|
+
for _ep in self._scan_java_file_for_entry_points(abs_path, rel_path):
|
|
556
|
+
if _ep.kind == "application":
|
|
557
|
+
if _ep.path in _app_paths:
|
|
558
|
+
continue
|
|
559
|
+
_app_paths.add(_ep.path)
|
|
560
|
+
annotation_eps.append(_ep)
|
|
542
561
|
entry_points.extend(annotation_eps)
|
|
543
562
|
|
|
544
563
|
# 3. web.xml servlet/filter declarations
|
|
@@ -611,6 +630,7 @@ class JavaDetector(AbstractDetector):
|
|
|
611
630
|
content = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
612
631
|
except OSError:
|
|
613
632
|
return None
|
|
633
|
+
content = _strip_java_comments(content)
|
|
614
634
|
if _BOOTSTRAP_ANNOTATION_RE.search(content):
|
|
615
635
|
return "bootstrap_annotation"
|
|
616
636
|
if _MAIN_METHOD_RE.search(content):
|
|
@@ -678,6 +698,20 @@ class JavaDetector(AbstractDetector):
|
|
|
678
698
|
and "ControllerAdvice" not in content
|
|
679
699
|
and not any(ann in content for ann in _CUSTOM_SECURITY_ANNOTATIONS)
|
|
680
700
|
and not has_jax_rs and not has_cdi and not has_spi):
|
|
701
|
+
# Not a controller/security/DI file — but it may still be an application
|
|
702
|
+
# entry point. The name-gated step-1 scan only reads *Application.java /
|
|
703
|
+
# *Main.java, so a real main(String[]) in a differently-named class (e.g.
|
|
704
|
+
# neo4j's Neo4jCommunity.java server bootstrap) would otherwise be invisible,
|
|
705
|
+
# leaving a client CLI named Main.java as the sole reported bootstrap. Reuse
|
|
706
|
+
# the content already read here; strip comments first so a main()/annotation
|
|
707
|
+
# in a javadoc example is never mistaken for a real entry point.
|
|
708
|
+
_decommented = _strip_java_comments(content)
|
|
709
|
+
if _BOOTSTRAP_ANNOTATION_RE.search(_decommented):
|
|
710
|
+
return [EntryPoint(path=rel_path, stack="java", kind="application",
|
|
711
|
+
source="bootstrap_annotation")]
|
|
712
|
+
if _MAIN_METHOD_RE.search(_decommented):
|
|
713
|
+
return [EntryPoint(path=rel_path, stack="java", kind="application",
|
|
714
|
+
source="main_method")]
|
|
681
715
|
return []
|
|
682
716
|
|
|
683
717
|
if _REST_CONTROLLER_RE.search(content):
|
|
@@ -30,7 +30,10 @@ ENDPOINT_SURFACE_RECONCILIATION: str = (
|
|
|
30
30
|
"handler after inheritance projection and JAX-RS sub-resource-locator composition. "
|
|
31
31
|
"It is NOT the canonical API surface — it still contains framework dynamic-admin "
|
|
32
32
|
"routes whose path is a Java FQN (e.g. /org.broadleafcommerce...), which the "
|
|
33
|
-
"`endpoints` command excludes
|
|
33
|
+
"`endpoints` command excludes, and endpoints from test-fixture / test-utility "
|
|
34
|
+
"modules (tagged scope=\"test_util\"), which the canonical CIR surface excludes so "
|
|
35
|
+
"impact-chain / pr-impact / spring-audit do not report test scaffolding as "
|
|
36
|
+
"production HTTP exposure. Endpoint counts differ across commands BY DESIGN "
|
|
34
37
|
"and are not contradictory; expected ordering for one repo is "
|
|
35
38
|
"repo-ir.route_surface (raw, includes FQN-shaped framework routes) >= "
|
|
36
39
|
"endpoints.total (canonical REST/API surface, FQN-shaped routes filtered out) >= "
|
|
@@ -120,6 +120,85 @@ def is_test_or_fixture_path(path: str) -> bool:
|
|
|
120
120
|
return False
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
# Infrastructure words that, combined with a delimited "test" token in the same
|
|
124
|
+
# directory segment, identify a module whose code is test scaffolding even though
|
|
125
|
+
# it lives under src/main (e.g. "server-test-utils", "foo-testkit"). Kept narrow
|
|
126
|
+
# on purpose: the "test" token must be present in the SAME segment, so packages
|
|
127
|
+
# named "utils"/"support"/"tools" alone never match.
|
|
128
|
+
_TEST_INFRA_TOKENS = frozenset({
|
|
129
|
+
"util", "utils", "fixture", "fixtures", "support",
|
|
130
|
+
"framework", "kit", "harness", "tools", "tool",
|
|
131
|
+
"helper", "helpers", "mock", "mocks", "stub", "stubs", "dummy",
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
# Glued (non-hyphenated) test-fixture module suffixes.
|
|
135
|
+
_TEST_INFRA_SUFFIXES = (
|
|
136
|
+
"testutils", "testutil", "testkit", "testfixtures", "testfixture",
|
|
137
|
+
"testsupport", "testframework", "testharness", "testtools",
|
|
138
|
+
"testhelpers", "testmocks", "teststubs",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def is_test_fixture_module_path(path: str) -> bool:
|
|
143
|
+
"""Return True when *path* belongs to a test-fixture / test-utility MODULE.
|
|
144
|
+
|
|
145
|
+
Structural, name-agnostic: a directory segment must contain a hyphen/underscore
|
|
146
|
+
-delimited ``test`` token together with an infrastructure word (util(s),
|
|
147
|
+
fixture(s), support, framework, kit, harness, tools, helper(s), mock(s),
|
|
148
|
+
stub(s), dummy) — e.g. ``server-test-utils``, ``foo-testkit``,
|
|
149
|
+
``bar-test-support`` — or match a known test-module segment outright.
|
|
150
|
+
|
|
151
|
+
This is deliberately NARROWER than :func:`is_test_or_fixture_path`: it targets
|
|
152
|
+
modules that ship HTTP resources, JAX-RS ``@Path`` fixtures, or dummy
|
|
153
|
+
controllers under ``src/main`` so they are not misreported as production API
|
|
154
|
+
surface. Requiring both tokens in one segment keeps legitimate ``utils`` /
|
|
155
|
+
``support`` / ``tools`` packages from ever matching.
|
|
156
|
+
"""
|
|
157
|
+
norm = path.replace("\\", "/").lower()
|
|
158
|
+
parts = norm.split("/")
|
|
159
|
+
for part in parts[:-1]: # skip the filename
|
|
160
|
+
bare = part.rstrip("/")
|
|
161
|
+
if bare in _TEST_MODULE_SEGMENTS:
|
|
162
|
+
return True
|
|
163
|
+
if bare.endswith(_TEST_INFRA_SUFFIXES):
|
|
164
|
+
return True
|
|
165
|
+
toks = bare.replace("_", "-").split("-")
|
|
166
|
+
if "test" in toks and any(t in _TEST_INFRA_TOKENS for t in toks):
|
|
167
|
+
return True
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
_EXAMPLE_MODULE_TOKENS = frozenset({
|
|
172
|
+
"example", "examples", "demo", "demos", "sample", "samples", "showcase",
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def is_example_module_path(path: str) -> bool:
|
|
177
|
+
"""Return True when *path* is inside an example/demo MODULE directory.
|
|
178
|
+
|
|
179
|
+
Structural: a directory segment above the source root carries a hyphen/underscore
|
|
180
|
+
-delimited example/demo token (e.g. ``eureka-examples/src/main/java/...``). Package
|
|
181
|
+
components below the source root (``io/confluent/examples/...``) are NOT matched —
|
|
182
|
+
only a module directory is a functional role; a package of the same name is not.
|
|
183
|
+
"""
|
|
184
|
+
norm = path.replace("\\", "/").lower()
|
|
185
|
+
# Prepend "/" so a relative path ("src/main/...") matches the same boundary as an
|
|
186
|
+
# absolute one; without this the head-truncation silently failed and package
|
|
187
|
+
# components below the source root (e.g. ".../samples/petclinic/...") false-matched.
|
|
188
|
+
probe = "/" + norm
|
|
189
|
+
head = norm
|
|
190
|
+
for root in ("/src/main/", "/src/test/", "/src/"):
|
|
191
|
+
idx = probe.find(root)
|
|
192
|
+
if idx >= 0:
|
|
193
|
+
head = probe[:idx]
|
|
194
|
+
break
|
|
195
|
+
for seg in head.split("/"):
|
|
196
|
+
toks = seg.replace("_", "-").split("-")
|
|
197
|
+
if any(t in _EXAMPLE_MODULE_TOKENS for t in toks):
|
|
198
|
+
return True
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
123
202
|
def is_vendor_path(path: str) -> bool:
|
|
124
203
|
"""Return True when *path* is inside a vendored / third-party directory.
|
|
125
204
|
|
|
@@ -25,7 +25,10 @@ from typing import Any, Iterable, Optional
|
|
|
25
25
|
from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
|
|
26
26
|
from sourcecode.endpoint_metrics import ENDPOINT_SURFACE_RECONCILIATION
|
|
27
27
|
from sourcecode.fqn_utils import normalize_owner_fqn as _normalize_owner_fqn
|
|
28
|
-
from sourcecode.path_filters import
|
|
28
|
+
from sourcecode.path_filters import (
|
|
29
|
+
is_test_path as _is_test_path,
|
|
30
|
+
is_test_fixture_module_path as _is_test_fixture_module_path,
|
|
31
|
+
)
|
|
29
32
|
from sourcecode.security_config import (
|
|
30
33
|
CustomSecuritySpec,
|
|
31
34
|
capture_markers as _capture_markers,
|
|
@@ -4176,6 +4179,23 @@ def _assemble(
|
|
|
4176
4179
|
_route_surface = _build_route_surface(
|
|
4177
4180
|
sorted_syms, route_diffs, extends_map=_extends_map, custom_security=custom_security
|
|
4178
4181
|
)
|
|
4182
|
+
# Tag routes that originate in a test-fixture / test-utility module (dummy web
|
|
4183
|
+
# services, JAX-RS @Path fixtures shipped under src/main of a *-test-utils /
|
|
4184
|
+
# testsuite / test-framework module). route_surface stays RAW evidence — nothing
|
|
4185
|
+
# is dropped here; the tag lets the CIR endpoint projection (ir_dict_to_canonical)
|
|
4186
|
+
# exclude them so every CIR consumer (impact-chain, pr-impact, review-pr,
|
|
4187
|
+
# spring-audit) shares one production-endpoint definition with the `endpoints`
|
|
4188
|
+
# command, while repo-ir can still see the full surface. Mirrors the scope tag
|
|
4189
|
+
# applied in extract_java_endpoints.
|
|
4190
|
+
_rs_fqn_file = {
|
|
4191
|
+
s.symbol: (s.source_file or s.declaring_file)
|
|
4192
|
+
for s in sorted_syms
|
|
4193
|
+
if s.type in ("class", "interface")
|
|
4194
|
+
}
|
|
4195
|
+
for _r in _route_surface:
|
|
4196
|
+
_rf = _rs_fqn_file.get(_r.get("effective_class", ""), "")
|
|
4197
|
+
if _rf and _is_test_fixture_module_path(_rf):
|
|
4198
|
+
_r["scope"] = "test_util"
|
|
4179
4199
|
_analysis_gaps = _compute_analysis_gaps(
|
|
4180
4200
|
sorted_syms, spring_summary, _route_surface, sorted_rels,
|
|
4181
4201
|
unparsed_type_files=unparsed_type_files,
|
|
@@ -5584,7 +5604,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5584
5604
|
"""
|
|
5585
5605
|
import re as _re
|
|
5586
5606
|
from typing import Any as _Any
|
|
5587
|
-
from sourcecode.path_filters import is_test_path
|
|
5607
|
+
from sourcecode.path_filters import is_test_path, is_test_fixture_module_path
|
|
5588
5608
|
|
|
5589
5609
|
_EXTENDS_FROM_SIG = _re.compile(r'\bextends\s+(\w+)')
|
|
5590
5610
|
|
|
@@ -5753,6 +5773,16 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5753
5773
|
endpoint_warnings = _recovery["warnings"]
|
|
5754
5774
|
_openapi_spec_path = _recovery["spec_path"]
|
|
5755
5775
|
|
|
5776
|
+
# Map each declared class/interface FQN to its source file so an endpoint can be
|
|
5777
|
+
# traced back to the MODULE that ships it. Used to flag endpoints that originate
|
|
5778
|
+
# in a test-fixture / test-utility module (dummy web services, JAX-RS @Path
|
|
5779
|
+
# fixtures) living under src/main — these are not production API surface.
|
|
5780
|
+
_fqn_to_file: dict[str, str] = {
|
|
5781
|
+
s.symbol: (s.source_file or s.declaring_file)
|
|
5782
|
+
for s in all_symbols
|
|
5783
|
+
if s.type in ("class", "interface")
|
|
5784
|
+
}
|
|
5785
|
+
|
|
5756
5786
|
endpoints: list[dict] = []
|
|
5757
5787
|
for route in routes:
|
|
5758
5788
|
handler = (
|
|
@@ -5769,6 +5799,9 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5769
5799
|
"handler": handler,
|
|
5770
5800
|
"return_type": route.get("return_type", "void"),
|
|
5771
5801
|
}
|
|
5802
|
+
_src = _fqn_to_file.get(route.get("effective_class", ""), "")
|
|
5803
|
+
if _src and is_test_fixture_module_path(_src):
|
|
5804
|
+
entry["scope"] = "test_util"
|
|
5772
5805
|
# Use security_annotations already extracted by _build_route_surface
|
|
5773
5806
|
# via the canonical _route_security_from_sym extractor.
|
|
5774
5807
|
security_info = route.get("security_annotations")
|
|
@@ -5791,6 +5824,14 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5791
5824
|
)
|
|
5792
5825
|
endpoints = [e for e in endpoints if not _FQN_PATH_RE.search(e.get("path", ""))]
|
|
5793
5826
|
|
|
5827
|
+
# Partition test-fixture/test-utility endpoints out of the production surface.
|
|
5828
|
+
# They ship under src/main of a *-test-utils / *-testkit / test-framework module
|
|
5829
|
+
# (dummy web services, JAX-RS @Path fixtures) and must not be reported as, or
|
|
5830
|
+
# counted with, the real API surface — but they are preserved in their own bucket
|
|
5831
|
+
# so nothing is silently dropped and the exclusion is auditable.
|
|
5832
|
+
_test_util_endpoints = [e for e in endpoints if e.get("scope") == "test_util"]
|
|
5833
|
+
endpoints = [e for e in endpoints if e.get("scope") != "test_util"]
|
|
5834
|
+
|
|
5794
5835
|
# "no_security_signal" = no recognized security annotation at method OR class level.
|
|
5795
5836
|
# Note: repos may use framework-level security (e.g. Keycloak itself) with no
|
|
5796
5837
|
# per-endpoint annotations — this count reflects annotation-based coverage only.
|
|
@@ -5919,6 +5960,16 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5919
5960
|
# Keep legacy field name for backward compat, now means same as no_security_signal
|
|
5920
5961
|
"undocumented": no_security_signal,
|
|
5921
5962
|
}
|
|
5963
|
+
# Surface test-fixture endpoints excluded from the production surface above.
|
|
5964
|
+
# Reported separately (not hidden) so the exclusion is transparent and auditable.
|
|
5965
|
+
if _test_util_endpoints:
|
|
5966
|
+
result["test_util_endpoints"] = _test_util_endpoints
|
|
5967
|
+
result["test_util_excluded"] = len(_test_util_endpoints)
|
|
5968
|
+
result.setdefault("notes", []).append(
|
|
5969
|
+
f"{len(_test_util_endpoints)} endpoint(s) from test-fixture/test-utility "
|
|
5970
|
+
f"module(s) under src/main were excluded from the production surface "
|
|
5971
|
+
f"(see test_util_endpoints)."
|
|
5972
|
+
)
|
|
5922
5973
|
# Surface incomplete-endpoint warnings (interface-defined controllers) only when
|
|
5923
5974
|
# present, to keep output backward-compatible for the common case.
|
|
5924
5975
|
if endpoint_warnings:
|
|
@@ -6366,6 +6417,12 @@ def compute_blast_radius(
|
|
|
6366
6417
|
|
|
6367
6418
|
endpoints_affected: list[dict] = []
|
|
6368
6419
|
for ep in route_surface:
|
|
6420
|
+
# Test-fixture routes (tagged by build_repo_ir) are not production HTTP
|
|
6421
|
+
# exposure — skip them so the legacy `impact` command shares the single
|
|
6422
|
+
# production-endpoint definition used by the CIR surfaces. Nothing else about
|
|
6423
|
+
# blast-radius selection changes; FQN-admin routes are untouched.
|
|
6424
|
+
if ep.get("scope") == "test_util":
|
|
6425
|
+
continue
|
|
6369
6426
|
ep_class = ep.get("effective_class") or ep.get("controller") or ep.get("class") or ""
|
|
6370
6427
|
ep_symbol = ep.get("symbol") or ""
|
|
6371
6428
|
ep_handler = (
|