sourcecode 2.1.0__tar.gz → 2.2.0__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.1.0 → sourcecode-2.2.0}/CHANGELOG.md +75 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/PKG-INFO +4 -4
- {sourcecode-2.1.0 → sourcecode-2.2.0}/README.md +3 -3
- {sourcecode-2.1.0 → sourcecode-2.2.0}/pyproject.toml +1 -1
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/__init__.py +1 -1
- sourcecode-2.2.0/src/sourcecode/caller_metrics.py +42 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/cli.py +118 -29
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/java.py +29 -1
- sourcecode-2.2.0/src/sourcecode/endpoint_metrics.py +42 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/explain.py +2 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/path_filters.py +2 -1
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/repository_ir.py +50 -14
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/semantic_integration_engine.py +18 -2
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/serializer.py +23 -3
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_semantic.py +18 -5
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_tx_analyzer.py +21 -6
- {sourcecode-2.1.0 → sourcecode-2.2.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/.gitignore +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/.ruff.toml +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/CONTRIBUTING.md +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/LICENSE +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/SECURITY.md +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/raw +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/license.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/functions/README.md +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.1.0 → sourcecode-2.2.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.2.0] — 2026-07-12
|
|
4
|
+
|
|
5
|
+
**Field-validation hardening — narrative/reporting-layer correctness across the
|
|
6
|
+
audit corpus.** A refute-first pass over ten real repositories fixed a batch of
|
|
7
|
+
mislabeling and miscount bugs where the headline framing contradicted the
|
|
8
|
+
structured output the engine had already produced. No Semantic IR change — every
|
|
9
|
+
fix is in the reporting/narrative layer and reuses facts already extracted.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`endpoints` — modeled JAX-RS no longer double-reported as un-modeled surface
|
|
14
|
+
(A2).** `_build_route_surface` fully models JAX-RS, but `extract_java_endpoints`
|
|
15
|
+
also counted every `@Path` file into `non_spring_rest_surface{modeled:false}` and
|
|
16
|
+
warned "count covers Spring-MVC-annotated endpoints only" — contradicting the
|
|
17
|
+
JAX-RS routes just emitted (18 on eureka, 689 on keycloak). Modeled routes now
|
|
18
|
+
carry framework provenance (`jax_rs`/`spring_mvc`); when JAX-RS routes were
|
|
19
|
+
modeled, `jax_rs` is dropped from the un-modeled surface. Genuinely un-modeled
|
|
20
|
+
surfaces (WebScripts/Servlets, or JAX-RS that produced no route) still surface and
|
|
21
|
+
still warn.
|
|
22
|
+
- **`spring-audit` — overloaded `@Transactional` methods counted separately (A1).**
|
|
23
|
+
`TransactionBoundaryIndex.by_symbol` is keyed by signature-less FQN, so overloaded
|
|
24
|
+
methods (`findAll()` / `findAll(Pageable)`) collapsed to one entry and `stats()`
|
|
25
|
+
undercounted. A new `all_declared` list (one entry per site) drives the count
|
|
26
|
+
(petclinic 1→2, matching source); FQN-keyed lookups are unchanged.
|
|
27
|
+
- **`spring-audit` — per-site TX overloads no longer hidden by `by_symbol` collapse
|
|
28
|
+
(A1-RESIDUAL).** TX-001 (proxy-bypass) and TX-005 (exception-swallow) are per-site
|
|
29
|
+
checks but iterated the collapsed FQN map; a non-risky overload could overwrite a
|
|
30
|
+
risky one sharing an FQN, silently dropping the finding. Both now iterate
|
|
31
|
+
`all_declared` with a dedupe guard. Corpus TX counts unchanged (the residual was
|
|
32
|
+
latent).
|
|
33
|
+
- **`compact` — Spring Boot version surfaced from pom property (A3).** `--compact`
|
|
34
|
+
deployment omitted the Boot version when it lived in a `<spring.boot.version>`
|
|
35
|
+
property rather than the starter-parent, while `migrate-check` resolved it. The
|
|
36
|
+
serializer now falls back to the property-resolving detector (single source of
|
|
37
|
+
truth), gated on Boot presence so non-Boot repos gain no phantom version
|
|
38
|
+
(Broadleaf 3.5.14, consistent).
|
|
39
|
+
- **`endpoints` — `Map.put("/", literal)` no longer treated as a router-DSL route
|
|
40
|
+
(B1).** The router-DSL detector matched an HTTP-verb name + `"/"` first arg; a
|
|
41
|
+
plain map write (`swapChars.put("/", "slash")`) was reported as a `PUT /`
|
|
42
|
+
endpoint. A negative lookahead now rejects a literal second argument — a handler
|
|
43
|
+
is never a bare literal (openmrs 3→2, jobrunr 12/12 kept).
|
|
44
|
+
- **`endpoints` — test-suite module controllers excluded from entry points (B2).**
|
|
45
|
+
A scaffolding `@Controller` in openmrs' `test-suite` module (sources under
|
|
46
|
+
`src/main`) surfaced as a production entry point. Entry-point collection now uses
|
|
47
|
+
the broader `is_test_or_fixture_path`, and `test-suite`/`test-suites` are
|
|
48
|
+
recognized test-module segments (openmrs 19→18; petclinic/eureka/keycloak
|
|
49
|
+
unchanged).
|
|
50
|
+
- **`stack` — JAX-RS API surfaced in the headline stack from source annotations
|
|
51
|
+
(C1).** Eureka reported `project_type=library` with an empty stack despite 18
|
|
52
|
+
JAX-RS routes, because the manifest scan has no Jersey/Guice/jsr311 vocabulary and
|
|
53
|
+
(for gradle) reads only the root build file. When the `.java` scan found JAX-RS
|
|
54
|
+
entry points and no Jakarta EE framework was inferred, the stack now surfaces
|
|
55
|
+
`Jakarta EE` from the `@Path`/`@GET` evidence (eureka → `Java rest api using
|
|
56
|
+
Jakarta EE`). Manifest-only jsr311 still infers nothing (honesty preserved).
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **`modernize` — cyclic tangles distinguished from one-way deps in
|
|
61
|
+
`cross_module_tangles` (TANGLE-1).** A directional dependency is normal layering,
|
|
62
|
+
not a tangle. Each entry now carries `coupling_type` (`cyclic` when mutual, else
|
|
63
|
+
`directional`); the note and recommendation state only cyclic entries are real
|
|
64
|
+
tangles. The key is retained (mcp/USER_GUIDE/tests reference it); cyclic entries
|
|
65
|
+
still sort first.
|
|
66
|
+
- **CLI — consistent progress spinner across all long-running commands (UX-1).** The
|
|
67
|
+
TTY-gated progress spinner ran on only 5 code paths; the slowest commands
|
|
68
|
+
(`spring-audit`, `validation`, `migrate-check`, `endpoints`, `export`,
|
|
69
|
+
`impact-chain`, `pr-impact`, `explain`) ran silent. All 9 now show the same
|
|
70
|
+
stderr-only, TTY/CI-gated indicator (never pollutes stdout JSON, zero cost in CI).
|
|
71
|
+
|
|
72
|
+
### Notes
|
|
73
|
+
|
|
74
|
+
- No breaking changes. All fixes are reporting/narrative-layer; the Semantic IR and
|
|
75
|
+
default JSON schemas are unchanged (`cross_module_tangles` gains an additive
|
|
76
|
+
`coupling_type` field). 3193 tests passing.
|
|
77
|
+
|
|
3
78
|
## [2.1.0] — 2026-07-10
|
|
4
79
|
|
|
5
80
|
**Robustness & Security Inference — vendor-agnostic security & validation posture.**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
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
|
|
@@ -38,11 +38,11 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
|
|
39
39
|
# ASK Engine
|
|
40
40
|
|
|
41
|
-
> **Persistent structural intelligence for AI coding agents
|
|
41
|
+
> **ASK — Actionable Software Knowledge.** Persistent structural intelligence for AI coding agents.
|
|
42
42
|
|
|
43
43
|
**Context · Impact · Migration · Architecture · Review — everything from one structural model.**
|
|
44
44
|
|
|
45
|
-

|
|
46
46
|

|
|
47
47
|
|
|
48
48
|
> **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
|
|
@@ -84,7 +84,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
|
|
|
84
84
|
# pip / pipx
|
|
85
85
|
pipx install sourcecode # or: pip install sourcecode
|
|
86
86
|
|
|
87
|
-
ask version # ask 2.
|
|
87
|
+
ask version # ask 2.2.0
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
> **Package vs. command.** The install package is named `sourcecode` this release
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# ASK Engine
|
|
2
2
|
|
|
3
|
-
> **Persistent structural intelligence for AI coding agents
|
|
3
|
+
> **ASK — Actionable Software Knowledge.** Persistent structural intelligence for AI coding agents.
|
|
4
4
|
|
|
5
5
|
**Context · Impact · Migration · Architecture · Review — everything from one structural model.**
|
|
6
6
|
|
|
7
|
-

|
|
8
8
|

|
|
9
9
|
|
|
10
10
|
> **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
|
|
@@ -46,7 +46,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
|
|
|
46
46
|
# pip / pipx
|
|
47
47
|
pipx install sourcecode # or: pip install sourcecode
|
|
48
48
|
|
|
49
|
-
ask version # ask 2.
|
|
49
|
+
ask version # ask 2.2.0
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
> **Package vs. command.** The install package is named `sourcecode` this release
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Single source of truth for the caller / fan-in metric reconciliation note.
|
|
2
|
+
|
|
3
|
+
Three commands report a "how many things reference this class" number, each
|
|
4
|
+
derived from a different graph projection. Read side by side they can look
|
|
5
|
+
contradictory (e.g. 215 vs 103 for the same class) — they are not. They count
|
|
6
|
+
different populations:
|
|
7
|
+
|
|
8
|
+
modernize.in_degree >= explain.incoming_callers ~= impact.stats.direct_caller_count
|
|
9
|
+
|
|
10
|
+
- ``modernize.in_degree`` counts graph *edges* (all edge types, symbol level, not
|
|
11
|
+
deduplicated per class) — a blast-radius ranking weight, always the largest, and
|
|
12
|
+
not a count of classes.
|
|
13
|
+
- ``explain.incoming_callers`` and ``impact.stats.direct_caller_count`` both count
|
|
14
|
+
*distinct dependent classes*. They normally match and are always the same order
|
|
15
|
+
of magnitude, but can differ by a small margin because each includes a slightly
|
|
16
|
+
different edge set (import-only references, DI-interface resolution). The
|
|
17
|
+
truncated ``impact.direct_callers`` array is a display sample, not the count.
|
|
18
|
+
|
|
19
|
+
Empirically verified across repositories (Broadleaf ExtensionManager: 215 / 103 /
|
|
20
|
+
103; petclinic entities & repositories: equal; occasional margin-of-one divergence
|
|
21
|
+
on classes with import-only references). The strict-equality claim was deliberately
|
|
22
|
+
NOT made — the note asserts only the robust ordering and the "same distinct-class
|
|
23
|
+
metric" relationship.
|
|
24
|
+
|
|
25
|
+
Every command that emits one of these numbers references this same string so the
|
|
26
|
+
explanation can never drift or contradict itself between commands.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
CALLER_METRIC_RECONCILIATION: str = (
|
|
30
|
+
"Fan-in / caller counts differ across commands BY DESIGN and are not "
|
|
31
|
+
"contradictory. For the same class the expected relationship is: "
|
|
32
|
+
"modernize.in_degree >= explain.incoming_callers ~= impact.stats.direct_caller_count. "
|
|
33
|
+
"modernize.in_degree = raw count of ALL incoming graph edges (imports, injects, "
|
|
34
|
+
"extends/implements, references, annotations), counted at symbol level and NOT "
|
|
35
|
+
"deduplicated per class — a blast-radius ranking weight, not a count of classes; "
|
|
36
|
+
"it is always the largest of the three. explain.incoming_callers and "
|
|
37
|
+
"impact.stats.direct_caller_count both count DISTINCT dependent classes "
|
|
38
|
+
"(deduplicated to class level); they normally match but can differ by a small "
|
|
39
|
+
"margin because each includes a slightly different edge set (e.g. import-only "
|
|
40
|
+
"references or DI-interface resolution). The top-level impact.direct_callers array "
|
|
41
|
+
"may be truncated for output size — use stats.direct_caller_count for the true total."
|
|
42
|
+
)
|
|
@@ -15,6 +15,7 @@ from sourcecode import __version__
|
|
|
15
15
|
from sourcecode.error_schema import INVALID_INPUT_CODE, build_error_envelope
|
|
16
16
|
from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
|
|
17
17
|
from sourcecode.progress import Progress
|
|
18
|
+
from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
|
|
18
19
|
from sourcecode.repository_ir import extract_java_endpoints as _extract_java_endpoints
|
|
19
20
|
|
|
20
21
|
|
|
@@ -700,7 +701,8 @@ _WELCOME_CMDS = (
|
|
|
700
701
|
|
|
701
702
|
def _print_welcome_plain(tier: str) -> None:
|
|
702
703
|
"""Plain-text welcome — fallback when rich is unavailable."""
|
|
703
|
-
lines = ["", f" ASK Engine {__version__} · {tier} · CLI: ask",
|
|
704
|
+
lines = ["", f" ASK Engine {__version__} · {tier} · CLI: ask",
|
|
705
|
+
" Actionable Software Knowledge Engine", "",
|
|
704
706
|
" AI coding-agent context, instant.", "", " Get started:"]
|
|
705
707
|
for cmd, desc in _WELCOME_CMDS:
|
|
706
708
|
lines.append(f" {cmd.ljust(34)}{desc}")
|
|
@@ -747,8 +749,9 @@ def _print_welcome() -> None:
|
|
|
747
749
|
t.append(" ")
|
|
748
750
|
t.append(tier, style=tier_style)
|
|
749
751
|
t.append("\n")
|
|
750
|
-
# "ENGINE" label beneath the wordmark.
|
|
751
|
-
t.append(_WELCOME_ENGINE
|
|
752
|
+
# "ENGINE" label beneath the wordmark, with the acronym expansion.
|
|
753
|
+
t.append(_WELCOME_ENGINE, style="bold dim cyan")
|
|
754
|
+
t.append(" Actionable Software Knowledge\n", style="dim")
|
|
752
755
|
|
|
753
756
|
t.append("\nAI coding-agent context, instant.\n\n", style="white")
|
|
754
757
|
|
|
@@ -3911,7 +3914,12 @@ def endpoints_cmd(
|
|
|
3911
3914
|
)
|
|
3912
3915
|
raise typer.Exit(code=1)
|
|
3913
3916
|
|
|
3914
|
-
|
|
3917
|
+
_prog = Progress()
|
|
3918
|
+
_prog.start("scanning endpoints")
|
|
3919
|
+
try:
|
|
3920
|
+
data = _extract_java_endpoints(target)
|
|
3921
|
+
finally:
|
|
3922
|
+
_prog.finish()
|
|
3915
3923
|
|
|
3916
3924
|
# Update RIS api_surface section (non-fatal side-effect).
|
|
3917
3925
|
try:
|
|
@@ -4358,6 +4366,9 @@ def export_cmd(
|
|
|
4358
4366
|
)
|
|
4359
4367
|
raise typer.Exit(1)
|
|
4360
4368
|
|
|
4369
|
+
_prog = Progress()
|
|
4370
|
+
_prog.start("scanning repository")
|
|
4371
|
+
|
|
4361
4372
|
file_list = [
|
|
4362
4373
|
f for f in find_java_files(root)
|
|
4363
4374
|
if "/test/" not in f and "/tests/" not in f
|
|
@@ -4384,6 +4395,7 @@ def export_cmd(
|
|
|
4384
4395
|
_integrations,
|
|
4385
4396
|
endpoint_meta=_ep_data,
|
|
4386
4397
|
)
|
|
4398
|
+
_prog.finish()
|
|
4387
4399
|
output = _serialize_dict(data, format)
|
|
4388
4400
|
_emit_command_output(output, output_path, copy,
|
|
4389
4401
|
success_msg=f"C4 architecture export written to {output_path}")
|
|
@@ -4413,6 +4425,7 @@ def export_cmd(
|
|
|
4413
4425
|
ContextGraph.build(file_list, root)
|
|
4414
4426
|
).as_report(len(file_list))
|
|
4415
4427
|
|
|
4428
|
+
_prog.finish()
|
|
4416
4429
|
output = _serialize_dict(data, format)
|
|
4417
4430
|
_emit_command_output(output, output_path, copy,
|
|
4418
4431
|
success_msg=f"Export written to {output_path}")
|
|
@@ -4495,6 +4508,8 @@ def validation_cmd(
|
|
|
4495
4508
|
|
|
4496
4509
|
from sourcecode.context_graph import ContextGraph
|
|
4497
4510
|
from sourcecode.validation_surface import build_validation_surface
|
|
4511
|
+
_prog = Progress()
|
|
4512
|
+
_prog.start("mapping validation surface")
|
|
4498
4513
|
# Structural facts come from the ContextGraph — the single access layer.
|
|
4499
4514
|
_graph = ContextGraph.build_from_root(target)
|
|
4500
4515
|
data = build_validation_surface(target, graph=_graph)
|
|
@@ -4527,6 +4542,7 @@ def validation_cmd(
|
|
|
4527
4542
|
if _note:
|
|
4528
4543
|
data["note"] = _note
|
|
4529
4544
|
|
|
4545
|
+
_prog.finish()
|
|
4530
4546
|
output = _serialize_dict(data, format)
|
|
4531
4547
|
_summary = data.get("summary", {})
|
|
4532
4548
|
# Human-facing heads-up when the result is empty purely because no OpenAPI
|
|
@@ -4750,6 +4766,8 @@ def spring_audit_cmd(
|
|
|
4750
4766
|
success_msg=f"Spring audit written to {output_path}")
|
|
4751
4767
|
return
|
|
4752
4768
|
|
|
4769
|
+
_prog = Progress()
|
|
4770
|
+
_prog.start(f"auditing {len(file_list)} Java files")
|
|
4753
4771
|
cir = ContextGraph.build(file_list, target).cir
|
|
4754
4772
|
_model = SpringSemanticModel.build(cir)
|
|
4755
4773
|
|
|
@@ -4802,6 +4820,7 @@ def spring_audit_cmd(
|
|
|
4802
4820
|
except Exception:
|
|
4803
4821
|
pass
|
|
4804
4822
|
|
|
4823
|
+
_prog.finish()
|
|
4805
4824
|
if format == "github-comment":
|
|
4806
4825
|
output = _render_spring_audit_github_comment(combined, min_severity)
|
|
4807
4826
|
else:
|
|
@@ -4920,7 +4939,12 @@ def migrate_check_cmd(
|
|
|
4920
4939
|
|
|
4921
4940
|
_file_limitations: list[str] = []
|
|
4922
4941
|
file_list = find_java_files(target, limitations=_file_limitations)
|
|
4923
|
-
|
|
4942
|
+
_prog = Progress()
|
|
4943
|
+
_prog.start(f"checking migration ({len(file_list)} files)")
|
|
4944
|
+
try:
|
|
4945
|
+
report = run_migrate_check(file_list, target, min_severity=min_severity)
|
|
4946
|
+
finally:
|
|
4947
|
+
_prog.finish()
|
|
4924
4948
|
if _file_limitations:
|
|
4925
4949
|
report.limitations.extend(_file_limitations)
|
|
4926
4950
|
|
|
@@ -5079,6 +5103,8 @@ def impact_chain_cmd(
|
|
|
5079
5103
|
success_msg=f"Impact chain written to {output_path}")
|
|
5080
5104
|
return
|
|
5081
5105
|
|
|
5106
|
+
_prog = Progress()
|
|
5107
|
+
_prog.start(f"analyzing impact ({len(file_list)} files)")
|
|
5082
5108
|
cir = ContextGraph.build(file_list, target).cir
|
|
5083
5109
|
_model = SpringSemanticModel.build(cir)
|
|
5084
5110
|
|
|
@@ -5086,6 +5112,7 @@ def impact_chain_cmd(
|
|
|
5086
5112
|
from sourcecode.spring_event_topology import run_event_topology
|
|
5087
5113
|
evt_result = run_event_topology(cir, symbol, model=_model)
|
|
5088
5114
|
data = evt_result.to_dict()
|
|
5115
|
+
_prog.finish()
|
|
5089
5116
|
output = _serialize_dict(data, format)
|
|
5090
5117
|
_emit_command_output(
|
|
5091
5118
|
output, output_path, copy,
|
|
@@ -5101,6 +5128,7 @@ def impact_chain_cmd(
|
|
|
5101
5128
|
result = run_impact_chain(cir, symbol, depth=depth, root=target, model=_model)
|
|
5102
5129
|
|
|
5103
5130
|
data = result.to_dict()
|
|
5131
|
+
_prog.finish()
|
|
5104
5132
|
output = _serialize_dict(data, format)
|
|
5105
5133
|
_emit_command_output(
|
|
5106
5134
|
output, output_path, copy,
|
|
@@ -5229,9 +5257,14 @@ def pr_impact_cmd(
|
|
|
5229
5257
|
success_msg=f"PR impact report written to {output_path}")
|
|
5230
5258
|
return
|
|
5231
5259
|
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5260
|
+
_prog = Progress()
|
|
5261
|
+
_prog.start(f"analyzing PR impact ({len(file_list)} files)")
|
|
5262
|
+
try:
|
|
5263
|
+
cir = ContextGraph.build(file_list, target).cir
|
|
5264
|
+
model = SpringSemanticModel.build(cir)
|
|
5265
|
+
report = run_pr_impact(cir, changed_files, root=target, model=model)
|
|
5266
|
+
finally:
|
|
5267
|
+
_prog.finish()
|
|
5235
5268
|
|
|
5236
5269
|
output = _serialize_dict(report.to_dict(), "json") if format == "json" else report.render_text()
|
|
5237
5270
|
_emit_command_output(
|
|
@@ -5348,9 +5381,14 @@ def explain_cmd(
|
|
|
5348
5381
|
)
|
|
5349
5382
|
raise typer.Exit(code=1)
|
|
5350
5383
|
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5384
|
+
_prog = Progress()
|
|
5385
|
+
_prog.start(f"explaining {class_name} ({len(file_list)} files)")
|
|
5386
|
+
try:
|
|
5387
|
+
cir = ContextGraph.build(file_list, target).cir
|
|
5388
|
+
model = SpringSemanticModel.build(cir)
|
|
5389
|
+
explanation = explain_class(class_name, cir, model)
|
|
5390
|
+
finally:
|
|
5391
|
+
_prog.finish()
|
|
5354
5392
|
|
|
5355
5393
|
if format == "json":
|
|
5356
5394
|
output = _json.dumps(explanation.to_dict(), indent=2, ensure_ascii=False)
|
|
@@ -5663,7 +5701,56 @@ def _partition_static_unreferenced(nodes: list[dict], root: Path) -> tuple[list[
|
|
|
5663
5701
|
if files_scanned >= _CONFIG_SCAN_MAX_FILES or not unresolved_simple:
|
|
5664
5702
|
break
|
|
5665
5703
|
|
|
5666
|
-
|
|
5704
|
+
# 3. Nested-type qualified-reference scan (SC-2 residual). The static call-graph
|
|
5705
|
+
# does not emit an edge for a nested-type member access in ordinary method-body
|
|
5706
|
+
# code — e.g. `PropertyType.AdminGroupPresentation.NAME` credits the enclosing
|
|
5707
|
+
# type, not the nested holder — so a referenced nested class can look
|
|
5708
|
+
# zero-degree. When a candidate is a nested type (its second-to-last FQN segment
|
|
5709
|
+
# is an UpperCamel enclosing type) and its qualified `Outer.Nested` form appears
|
|
5710
|
+
# in some OTHER Java source, it is statically referenced: neither dead nor
|
|
5711
|
+
# framework-dispatched. Structural, name-agnostic (pattern derived from the
|
|
5712
|
+
# candidate's own FQN).
|
|
5713
|
+
import re as _re_nested
|
|
5714
|
+
statically_referenced: set[str] = set()
|
|
5715
|
+
nested_patterns: dict[str, "tuple"] = {}
|
|
5716
|
+
for n in nodes:
|
|
5717
|
+
if n["fqn"] in dispatched_fqns:
|
|
5718
|
+
continue
|
|
5719
|
+
parts = (n.get("fqn") or "").split(".")
|
|
5720
|
+
if len(parts) >= 2 and parts[-2][:1].isupper():
|
|
5721
|
+
outer, nested = parts[-2], parts[-1]
|
|
5722
|
+
nested_patterns[n["fqn"]] = (
|
|
5723
|
+
_re_nested.compile(r"\b" + _re_nested.escape(outer) + r"\." + _re_nested.escape(nested) + r"\b"),
|
|
5724
|
+
n.get("source_file") or "",
|
|
5725
|
+
)
|
|
5726
|
+
if nested_patterns:
|
|
5727
|
+
files_scanned = 0
|
|
5728
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
5729
|
+
dirnames[:] = [d for d in dirnames
|
|
5730
|
+
if d not in {".git", "build", "out", "target", "node_modules", ".gradle"}]
|
|
5731
|
+
for fname in filenames:
|
|
5732
|
+
if not fname.endswith(".java"):
|
|
5733
|
+
continue
|
|
5734
|
+
if files_scanned >= _CONFIG_SCAN_MAX_FILES or len(statically_referenced) == len(nested_patterns):
|
|
5735
|
+
break
|
|
5736
|
+
fpath = os.path.join(dirpath, fname)
|
|
5737
|
+
rel = os.path.relpath(fpath, root)
|
|
5738
|
+
try:
|
|
5739
|
+
with open(fpath, "r", encoding="utf-8", errors="replace") as fh:
|
|
5740
|
+
text = fh.read(_CONFIG_SCAN_MAX_BYTES)
|
|
5741
|
+
except OSError:
|
|
5742
|
+
continue
|
|
5743
|
+
files_scanned += 1
|
|
5744
|
+
for fqn, (pat, own) in nested_patterns.items():
|
|
5745
|
+
if fqn in statically_referenced or rel == own:
|
|
5746
|
+
continue
|
|
5747
|
+
if pat.search(text):
|
|
5748
|
+
statically_referenced.add(fqn)
|
|
5749
|
+
if files_scanned >= _CONFIG_SCAN_MAX_FILES or len(statically_referenced) == len(nested_patterns):
|
|
5750
|
+
break
|
|
5751
|
+
|
|
5752
|
+
unreferenced = [n for n in nodes
|
|
5753
|
+
if n["fqn"] not in dispatched_fqns and n["fqn"] not in statically_referenced]
|
|
5667
5754
|
dispatched = [n for n in nodes if n["fqn"] in dispatched_fqns]
|
|
5668
5755
|
return unreferenced, dispatched
|
|
5669
5756
|
|
|
@@ -5870,8 +5957,13 @@ def modernize_cmd(
|
|
|
5870
5957
|
"edge_count": _cnt,
|
|
5871
5958
|
"reverse_edge_count": _reverse,
|
|
5872
5959
|
# A mutual (cyclic) dependency is the actual "tangle" — both modules
|
|
5873
|
-
# reference each other, so neither can be extracted independently.
|
|
5960
|
+
# reference each other, so neither can be extracted independently. A
|
|
5961
|
+
# one-way (directional) dependency is normal layering, NOT a tangle:
|
|
5962
|
+
# TANGLE-1 (petclinic field test) surfaced owner→model / vet→model
|
|
5963
|
+
# (reverse_edge_count:0) being labelled "tangles" when they are plain
|
|
5964
|
+
# base-model usage. coupling_type states which it is, honestly.
|
|
5874
5965
|
"mutual": _reverse > 0,
|
|
5966
|
+
"coupling_type": "cyclic" if _reverse > 0 else "directional",
|
|
5875
5967
|
"example": _pair_example.get((_a, _b), ""),
|
|
5876
5968
|
})
|
|
5877
5969
|
# Surface mutual tangles first (highest decomposition risk), then by strength.
|
|
@@ -5947,17 +6039,10 @@ def modernize_cmd(
|
|
|
5947
6039
|
{"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
|
|
5948
6040
|
for n in coupling_nodes
|
|
5949
6041
|
],
|
|
5950
|
-
#
|
|
5951
|
-
# so it is not confused with `explain`'s caller
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
"(imports, injects, extends/implements, references, annotations), "
|
|
5955
|
-
"counted at symbol level. This is deliberately larger than and NOT "
|
|
5956
|
-
"equal to `ask explain`'s caller count, which reports DISTINCT "
|
|
5957
|
-
"dependent classes (DI dependents + reverse-call-graph callers, "
|
|
5958
|
-
"deduplicated to class level). Use in_degree for blast-radius ranking, "
|
|
5959
|
-
"explain's caller list for the concrete dependents to inspect."
|
|
5960
|
-
),
|
|
6042
|
+
# SC-6 (Broadleaf field test): make the in_degree metric self-describing
|
|
6043
|
+
# so it is not confused with `explain`'s / `impact`'s caller counts. All
|
|
6044
|
+
# three share one reconciliation note so they can never contradict.
|
|
6045
|
+
"high_coupling_nodes_note": CALLER_METRIC_RECONCILIATION,
|
|
5961
6046
|
"statically_unreferenced": [
|
|
5962
6047
|
{"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
|
|
5963
6048
|
for n in dead_zones
|
|
@@ -5978,10 +6063,12 @@ def modernize_cmd(
|
|
|
5978
6063
|
"cross_module_tangles_note": (
|
|
5979
6064
|
"Directed inter-subsystem coupling measured from structural graph "
|
|
5980
6065
|
"edges (imports/injects/extends/implements/references/calls). "
|
|
5981
|
-
"edge_count = edges from_package→to_package
|
|
5982
|
-
"
|
|
5983
|
-
"independent module extraction
|
|
5984
|
-
"
|
|
6066
|
+
"edge_count = edges from_package→to_package. coupling_type=cyclic "
|
|
6067
|
+
"(mutual=true) is a bidirectional dependency — the actual tangle "
|
|
6068
|
+
"that blocks independent module extraction; coupling_type="
|
|
6069
|
+
"directional (reverse_edge_count=0) is a normal one-way layered "
|
|
6070
|
+
"dependency, listed for coupling strength, NOT a tangle. Empty "
|
|
6071
|
+
"means no cross-subsystem structural coupling was detected."
|
|
5985
6072
|
),
|
|
5986
6073
|
# BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
|
|
5987
6074
|
"recommendation": (
|
|
@@ -5992,7 +6079,9 @@ def modernize_cmd(
|
|
|
5992
6079
|
)
|
|
5993
6080
|
+ "statically_unreferenced lists classes with no Java callers — review "
|
|
5994
6081
|
+ "for framework dispatch (XML/reflection/SPI) before removing. "
|
|
5995
|
-
+ "
|
|
6082
|
+
+ "In cross_module_tangles, coupling_type=cyclic entries are the "
|
|
6083
|
+
+ "real tangles to decompose first; directional entries are normal "
|
|
6084
|
+
+ "layering ranked by coupling strength."
|
|
5996
6085
|
),
|
|
5997
6086
|
}
|
|
5998
6087
|
|
|
@@ -176,6 +176,28 @@ class JavaDetector(AbstractDetector):
|
|
|
176
176
|
frameworks = [f for f in frameworks if f.name != "MyBatis"]
|
|
177
177
|
|
|
178
178
|
entry_points = self._collect_entry_points(context)
|
|
179
|
+
|
|
180
|
+
# C1 (Eureka field test): a repo whose REST surface is JAX-RS / Jakarta
|
|
181
|
+
# REST — declared on the source via @Path/@GET, not through a web framework
|
|
182
|
+
# coordinate in the build manifest — must still surface that in the headline
|
|
183
|
+
# stack. Eureka wires JAX-RS through Jersey 1.x (com.sun.jersey) + jsr311-api
|
|
184
|
+
# + Guice, none of which the manifest-coordinate scan models, and its child
|
|
185
|
+
# build.gradle files are not scanned at all — yet it ships 18 @Path resource
|
|
186
|
+
# classes. Without this the headline read "Java library / package" while the
|
|
187
|
+
# endpoints command listed a full JAX-RS API. When the annotation scan found
|
|
188
|
+
# JAX-RS entry points and no Jakarta-EE framework was inferred from the
|
|
189
|
+
# manifest, surface it so the stack (and project_type) reflect the REST API
|
|
190
|
+
# the endpoints already prove. Structural (VAI): reasons from the JAX-RS
|
|
191
|
+
# entry points found in source, not from a vendor dependency-name allowlist.
|
|
192
|
+
# detected_via carries no path form, so it is treated as repo-wide evidence
|
|
193
|
+
# (never localized to an adapter submodule).
|
|
194
|
+
if not any(f.name == "Jakarta EE" for f in frameworks) and any(
|
|
195
|
+
ep.kind in ("jax_rs_controller", "jax_rs_provider") for ep in entry_points
|
|
196
|
+
):
|
|
197
|
+
frameworks.append(FrameworkDetection(
|
|
198
|
+
name="Jakarta EE", source="annotation", detected_via=["jax_rs_endpoints"],
|
|
199
|
+
))
|
|
200
|
+
|
|
179
201
|
transactional_classes = self._collect_transactional_classes(context, all_paths)
|
|
180
202
|
stack = StackDetection(
|
|
181
203
|
stack="java",
|
|
@@ -459,7 +481,13 @@ class JavaDetector(AbstractDetector):
|
|
|
459
481
|
# 1. @SpringBootApplication entry: Application.java / Main.java by name
|
|
460
482
|
# Exclude test trees: test helpers like AdminApplication.java in
|
|
461
483
|
# integration/src/test/java/ must not be treated as production entrypoints.
|
|
462
|
-
|
|
484
|
+
# B2 (openmrs field test): exclude not just src/test/ trees but whole
|
|
485
|
+
# test-harness MODULES whose sources sit under src/main — e.g. openmrs'
|
|
486
|
+
# `test-suite/module/omod/src/main/java/.../TestModuleController.java`, a
|
|
487
|
+
# scaffolding @Controller (`GET /module/testmodule/hello`) that a narrow
|
|
488
|
+
# src/test/ check misses, so it surfaced as a production entry point. The
|
|
489
|
+
# broader is_test_or_fixture_path catches test-suite / *-test / *-it modules.
|
|
490
|
+
from sourcecode.path_filters import is_test_or_fixture_path as _is_test_path
|
|
463
491
|
# BUG #3 (Alfresco field test): a `*Application.java` / `*Main.java` NAME does
|
|
464
492
|
# not make a file a bootstrap entry point. Alfresco has XSD-generated JAXB
|
|
465
493
|
# `Application.java` model classes (no main(), no bootstrap annotation) in a
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Single source of truth for the endpoint / route-surface reconciliation note.
|
|
2
|
+
|
|
3
|
+
Three commands report an endpoint count, each from a different point in the
|
|
4
|
+
pipeline. Read side by side they look contradictory (e.g. 245 vs 225 vs 220 for
|
|
5
|
+
one repo) — they are not. Expected ordering for a single repo:
|
|
6
|
+
|
|
7
|
+
repo-ir.route_surface >= endpoints.total >= spring-audit.endpoints_analyzed
|
|
8
|
+
|
|
9
|
+
- ``repo-ir.route_surface`` is the RAW structural surface (every @RequestMapping /
|
|
10
|
+
@Path handler after inheritance projection and JAX-RS sub-resource-locator
|
|
11
|
+
composition). It still contains framework dynamic-admin routes whose path is a
|
|
12
|
+
Java FQN (e.g. ``/org.broadleafcommerce.core.search.domain.FieldImpl``), which
|
|
13
|
+
the ``endpoints`` command filters out — so it is NOT the canonical API surface.
|
|
14
|
+
- ``endpoints.total`` is the canonical REST/API surface (FQN-shaped framework
|
|
15
|
+
routes removed). This is the number to cite for "how many endpoints".
|
|
16
|
+
- ``spring-audit.endpoints_analyzed`` is that canonical surface further deduplicated
|
|
17
|
+
by (method, path, controller, handler) for security analysis.
|
|
18
|
+
|
|
19
|
+
The exact arithmetic between the three is intentionally NOT asserted (the pipelines
|
|
20
|
+
differ in dedup/expansion, so route_surface minus FQN-paths does not equal
|
|
21
|
+
endpoints.total). Only the robust ordering and the population of each are stated.
|
|
22
|
+
|
|
23
|
+
``spring-audit`` already documents its own count inline; this note gives repo-ir's
|
|
24
|
+
raw ``route_surface`` the same self-description so no reader mistakes it for the
|
|
25
|
+
canonical endpoint total.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
ENDPOINT_SURFACE_RECONCILIATION: str = (
|
|
29
|
+
"route_surface is the RAW structural route surface: every @RequestMapping/@Path "
|
|
30
|
+
"handler after inheritance projection and JAX-RS sub-resource-locator composition. "
|
|
31
|
+
"It is NOT the canonical API surface — it still contains framework dynamic-admin "
|
|
32
|
+
"routes whose path is a Java FQN (e.g. /org.broadleafcommerce...), which the "
|
|
33
|
+
"`endpoints` command excludes. Endpoint counts differ across commands BY DESIGN "
|
|
34
|
+
"and are not contradictory; expected ordering for one repo is "
|
|
35
|
+
"repo-ir.route_surface (raw, includes FQN-shaped framework routes) >= "
|
|
36
|
+
"endpoints.total (canonical REST/API surface, FQN-shaped routes filtered out) >= "
|
|
37
|
+
"spring-audit.metadata.endpoints_analyzed (canonical surface further deduplicated "
|
|
38
|
+
"by (method, path, controller, handler) for security analysis). Cite "
|
|
39
|
+
"endpoints.total for the real API surface; the pipelines differ in dedup and "
|
|
40
|
+
"prefix expansion, so the three counts are related by this ordering, not by exact "
|
|
41
|
+
"arithmetic."
|
|
42
|
+
)
|
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
from dataclasses import dataclass, field
|
|
18
18
|
from typing import TYPE_CHECKING, Optional
|
|
19
19
|
|
|
20
|
+
from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
|
|
20
21
|
from sourcecode.fqn_utils import normalize_owner_fqn
|
|
21
22
|
|
|
22
23
|
if TYPE_CHECKING:
|
|
@@ -121,6 +122,7 @@ class ClassExplanation:
|
|
|
121
122
|
"purpose": self.purpose,
|
|
122
123
|
"public_methods": self.public_methods,
|
|
123
124
|
"incoming_callers": self.incoming_callers,
|
|
125
|
+
"incoming_callers_note": CALLER_METRIC_RECONCILIATION,
|
|
124
126
|
"outgoing_deps": self.outgoing_deps,
|
|
125
127
|
"events_published": self.events_published,
|
|
126
128
|
"events_consumed": self.events_consumed,
|
|
@@ -16,7 +16,8 @@ _TEST_SEGMENTS = frozenset({
|
|
|
16
16
|
# code lives under src/main (e.g. a Maven module that ships a test framework).
|
|
17
17
|
# A finding under one of these modules is test infrastructure, not the product.
|
|
18
18
|
_TEST_MODULE_SEGMENTS = frozenset({
|
|
19
|
-
"testsuite", "test-
|
|
19
|
+
"testsuite", "test-suite", "test-suites",
|
|
20
|
+
"test-framework", "testframework",
|
|
20
21
|
"integration-arquillian", "arquillian",
|
|
21
22
|
"test-utils", "test-util", "testutils",
|
|
22
23
|
"test-support", "testsupport",
|