sourcecode 2.5.18__tar.gz → 2.5.20__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.18 → sourcecode-2.5.20}/CHANGELOG.md +72 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/PKG-INFO +1 -1
- {sourcecode-2.5.18 → sourcecode-2.5.20}/pyproject.toml +1 -1
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/__init__.py +1 -1
- sourcecode-2.5.20/src/sourcecode/architectural_delta.py +147 -0
- sourcecode-2.5.20/src/sourcecode/change_plan.py +195 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/cli.py +261 -0
- sourcecode-2.5.20/src/sourcecode/contract_diff.py +186 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/format_contract.py +2 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/repository_ir.py +47 -9
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/validation_inference.py +94 -24
- {sourcecode-2.5.18 → sourcecode-2.5.20}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/.gitignore +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/.ruff.toml +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/CONTRIBUTING.md +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/LICENSE +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/README.md +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/SECURITY.md +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/raw +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/scripts/perf_harness.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/archetype.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/classifier.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/explain.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/license.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/perf.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/serializer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/supabase/functions/README.md +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.5.18 → sourcecode-2.5.20}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.5.20] — 2026-07-21
|
|
4
|
+
|
|
5
|
+
**Engineering Decision Support — Part A, F3: JAX-RS composed-path canonicalization.**
|
|
6
|
+
Composed `@Path` templates built on a constant whose own value is a concatenation
|
|
7
|
+
(`ROOT_PATH = "/{" + DB_PATH_PARAM_NAME + "}/query/v2"`) produced malformed endpoint
|
|
8
|
+
paths — the class prefix collapsed to `/{`, yielding `/{`, `/{/tx`, `/{/tx/{txId}`.
|
|
9
|
+
Correct endpoint identity is the key of every impact / blast-radius / delta decision,
|
|
10
|
+
so a malformed path corrupts each of them. A pure extraction-correctness fix (no new
|
|
11
|
+
inference). Full suite (3545 passed, 5 skipped) with zero control-repo drift.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **F3 — `_collect_file_constants` now folds concat / constant-ref constant values.**
|
|
16
|
+
The collector captured only the FIRST string literal of a `static final String`
|
|
17
|
+
declaration (`_STATIC_FINAL_STR_RE`), so a constant defined as an expression
|
|
18
|
+
(`"/{" + DB_PATH_PARAM_NAME + "}/query/v2"`) was stored truncated as `/{`. Every
|
|
19
|
+
`@Path(SomeResource.ROOT_PATH)` class prefix built on it then collapsed, and each
|
|
20
|
+
composed method path inherited the collapse. The collector now captures the full
|
|
21
|
+
right-hand side (`_STATIC_FINAL_STR_DECL_RE`, up to `;`) and resolves it through
|
|
22
|
+
the existing `_resolve_const_concat`, iterating to a fixpoint so a constant defined
|
|
23
|
+
in terms of another (in either declaration order) resolves. Inline method-level
|
|
24
|
+
concat (`@Path("tx/{" + TX_ID_PATH_PARAM_NAME + "}")`) already resolved via
|
|
25
|
+
`_resolve_ann_path_expr`; this was the one remaining gap. Measured on neo4j
|
|
26
|
+
(`QueryResource`): 8 malformed paths → canonical (`/{databaseName}/query/v2`,
|
|
27
|
+
`/{databaseName}/query/v2/tx/{txId}/commit`). Fleet A/B: neo4j corrected, the other
|
|
28
|
+
7 control repos byte-identical — the fix only ever *completes* a truncated value,
|
|
29
|
+
never rewrites a literal one.
|
|
30
|
+
|
|
31
|
+
## [2.5.19] — 2026-07-20
|
|
32
|
+
|
|
33
|
+
**Engineering Decision Support — Part A, F2: manual validation (guard clauses) is
|
|
34
|
+
now a recognised validation source.** A repo that validates request bodies with
|
|
35
|
+
hand-written null/empty guards instead of Bean Validation was reported as having no
|
|
36
|
+
validation — a confirmed false negative. F2 reuses the β `GuardClause` atom already
|
|
37
|
+
in the Semantic IR rather than adding a parallel extractor. Full suite (3542 passed,
|
|
38
|
+
5 skipped) confirms confinement.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **F2 — β guard-clause extraction now covers endpoint-kind handler bodies.**
|
|
43
|
+
`_extract_guard_facts` scanned only `symbol_kind in {method, constructor}`; an
|
|
44
|
+
annotation-classified handler (`@PostMapping …`) surfaces as `symbol_kind="endpoint"`,
|
|
45
|
+
so **every guard living in a handler body was silently dropped** — the β atom
|
|
46
|
+
existed, its coverage did not. The scan now includes endpoint-kind symbols (the same
|
|
47
|
+
`_scan_guard_clauses`, widened to a kind it skipped). This also lets the impact
|
|
48
|
+
engine see handler-entry guards it was previously blind to (measured: additive
|
|
49
|
+
behavioural notes only, each backed by a real atom — e.g. keycloak 34 handler types
|
|
50
|
+
gain a "null/empty guard" note; petclinic unchanged).
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- **F2 — `manual_validation` request-body pattern in `validation_inference`.** When no
|
|
55
|
+
Bean Validation is present but body handlers guard their input with null/empty β
|
|
56
|
+
guard clauses that early-exit, the `validation` command now classifies
|
|
57
|
+
`manual_validation` (Likely) with a per-endpoint `manual_guard` finding, instead of
|
|
58
|
+
the old `no_validation_detected`. Scoped to `null_check`/`empty_check`: β's `other`
|
|
59
|
+
test kind lumps range checks with non-validation guards (availability, memoization,
|
|
60
|
+
preconditions), so counting it would inflate false positives — range validation is an
|
|
61
|
+
honest, documented under-report. Bean validation still takes repo-level precedence; a
|
|
62
|
+
handler that both `@Valid`-s and hand-guards is bean-validated, not manual.
|
|
63
|
+
|
|
64
|
+
### Validation (real-repo field check)
|
|
65
|
+
|
|
66
|
+
- **True positive** — eureka `ApplicationResource#addInstance` (POST) validates its
|
|
67
|
+
request body with an `isBlank(...)` / `== null` guard cascade; previously reported as
|
|
68
|
+
unvalidated, now `manual_validation`.
|
|
69
|
+
- **True negative preserved** — neo4j stays `no_validation_detected`: its handler guards
|
|
70
|
+
are database-availability checks (`if (isDatabaseNotAvailable(db)) return`, β
|
|
71
|
+
`other`), which the `other`-exclusion correctly refuses to call input validation.
|
|
72
|
+
- **No drift** — petclinic stays `bean_validation_active`, jobrunr stays
|
|
73
|
+
`no_validation_detected`.
|
|
74
|
+
|
|
3
75
|
## [2.5.18] — 2026-07-20
|
|
4
76
|
|
|
5
77
|
**Engineering Decision Support — Part A, F1: standalone OpenAPI specs now reach the
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""architectural_delta.py — D1 Architectural Delta (before/after outcome measurement).
|
|
2
|
+
|
|
3
|
+
Engineering Decision Support, Part B / D1. ASK measures repository *state*; this
|
|
4
|
+
module measures the *engineering outcome* of a change by diffing two already-built
|
|
5
|
+
canonical IR snapshots and reporting the DELTAS — files, symbols, endpoints,
|
|
6
|
+
dependency edges, HTTP-surface set changes, and per-symbol fan-in shifts.
|
|
7
|
+
|
|
8
|
+
Discipline (moat line): every number here is MEASURED and derived from the CIR
|
|
9
|
+
alone. This layer computes deltas and stops — it never opines "better/worse",
|
|
10
|
+
never assigns ROI, and never classifies a change as breaking/non-breaking (that is
|
|
11
|
+
D2's contract-diff, a distinct axis). A reviewer reads the deltas and decides.
|
|
12
|
+
|
|
13
|
+
Determinism: `extract_metrics` reads only the CIR; `diff_metrics` is a pure
|
|
14
|
+
function of two metric snapshots. Same two snapshots → byte-identical delta.
|
|
15
|
+
|
|
16
|
+
Increment scope (D1-a): counts + HTTP-surface set delta + fan-in shifts. Import
|
|
17
|
+
cycles and blast-radius deltas are deferred to a later increment (heavier compute,
|
|
18
|
+
own metric extractor) — omitted here rather than approximated.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from sourcecode.canonical_ir import CanonicalRepositoryIR
|
|
27
|
+
|
|
28
|
+
# Frozen schema tag — versioned like perf-baseline-v1 so consumers can pin it.
|
|
29
|
+
ARCH_DELTA_SCHEMA: str = "architectural-delta-v1"
|
|
30
|
+
|
|
31
|
+
# Cap on the length of any per-item list in the payload (added/removed endpoints,
|
|
32
|
+
# fan-in shifts). The counts are always exact; the lists are a bounded sample.
|
|
33
|
+
_LIST_CAP: int = 50
|
|
34
|
+
|
|
35
|
+
# reverse_graph edge types that are NOT a dependency. `contained_in` is pure
|
|
36
|
+
# structural membership (a class "contains" its own methods/ctor) — counting it
|
|
37
|
+
# would make every class report a fan-in equal to its own member count (a
|
|
38
|
+
# controller nobody calls would read as fan-in 2). Every other type
|
|
39
|
+
# (calls/injects/extends/implements/imports/returns/instantiates/references) is a
|
|
40
|
+
# genuine reverse dependency and IS counted.
|
|
41
|
+
_FAN_IN_EXCLUDE_EDGE_TYPES: frozenset[str] = frozenset({"contained_in"})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _fan_in_map(cir: "CanonicalRepositoryIR") -> dict[str, int]:
|
|
45
|
+
"""symbol FQN → distinct reverse-dependant count, from the reverse graph.
|
|
46
|
+
|
|
47
|
+
reverse_graph is target → {edge_type → [dependant_fqn, ...]}. Fan-in is the
|
|
48
|
+
number of unique dependants across all DEPENDENCY edge types (a dependant that
|
|
49
|
+
both calls and injects a target counts once); structural containment is
|
|
50
|
+
excluded (see `_FAN_IN_EXCLUDE_EDGE_TYPES`)."""
|
|
51
|
+
out: dict[str, int] = {}
|
|
52
|
+
for target, by_type in (cir.reverse_graph or {}).items():
|
|
53
|
+
deps: set[str] = set()
|
|
54
|
+
for edge_type, dep_list in (by_type or {}).items():
|
|
55
|
+
if edge_type in _FAN_IN_EXCLUDE_EDGE_TYPES:
|
|
56
|
+
continue
|
|
57
|
+
deps.update(dep_list or ())
|
|
58
|
+
out[target] = len(deps)
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class ArchMetrics:
|
|
64
|
+
"""A deterministic architectural snapshot extracted from one CIR."""
|
|
65
|
+
|
|
66
|
+
cir_hash: str
|
|
67
|
+
file_count: int
|
|
68
|
+
symbol_count: int
|
|
69
|
+
endpoint_count: int
|
|
70
|
+
dependency_edge_count: int
|
|
71
|
+
endpoints: frozenset[tuple[str, str]] # (METHOD, path) — the HTTP contract surface
|
|
72
|
+
fan_in: dict[str, int] # symbol FQN → distinct caller count
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def extract_metrics(cir: "CanonicalRepositoryIR") -> ArchMetrics:
|
|
76
|
+
"""Project a CIR into its measured architectural metrics. Reads the CIR only."""
|
|
77
|
+
endpoints = frozenset(
|
|
78
|
+
(str(ep.method).upper(), str(ep.path)) for ep in (cir.endpoints or [])
|
|
79
|
+
)
|
|
80
|
+
return ArchMetrics(
|
|
81
|
+
cir_hash=cir.cir_hash,
|
|
82
|
+
file_count=len(cir.files or []),
|
|
83
|
+
symbol_count=len(cir.symbols or []),
|
|
84
|
+
endpoint_count=len(cir.endpoints or []),
|
|
85
|
+
dependency_edge_count=len(cir.dependencies or []),
|
|
86
|
+
endpoints=endpoints,
|
|
87
|
+
fan_in=_fan_in_map(cir),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _count_block(base: int, head: int) -> dict:
|
|
92
|
+
return {"base": base, "head": head, "delta": head - base}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def diff_metrics(base: ArchMetrics, head: ArchMetrics) -> dict:
|
|
96
|
+
"""Pure diff of two snapshots → the frozen architectural-delta-v1 payload.
|
|
97
|
+
|
|
98
|
+
Deterministic: added/removed endpoints are sorted; fan-in shifts are ranked by
|
|
99
|
+
magnitude then symbol name. Lists are capped; counts are exact."""
|
|
100
|
+
added = sorted(f"{m} {p}" for (m, p) in (head.endpoints - base.endpoints))
|
|
101
|
+
removed = sorted(f"{m} {p}" for (m, p) in (base.endpoints - head.endpoints))
|
|
102
|
+
|
|
103
|
+
# Fan-in shifts: any symbol whose distinct-caller count changed (present in
|
|
104
|
+
# either snapshot; absent = 0). Ranked by |delta| desc, then FQN asc.
|
|
105
|
+
shifts: list[dict] = []
|
|
106
|
+
for sym in set(base.fan_in) | set(head.fan_in):
|
|
107
|
+
b = base.fan_in.get(sym, 0)
|
|
108
|
+
h = head.fan_in.get(sym, 0)
|
|
109
|
+
if b != h:
|
|
110
|
+
shifts.append({"symbol": sym, "base": b, "head": h, "delta": h - b})
|
|
111
|
+
shifts.sort(key=lambda s: (-abs(s["delta"]), s["symbol"]))
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"schema": ARCH_DELTA_SCHEMA,
|
|
115
|
+
"base_cir_hash": base.cir_hash,
|
|
116
|
+
"head_cir_hash": head.cir_hash,
|
|
117
|
+
"totals": {
|
|
118
|
+
"files": _count_block(base.file_count, head.file_count),
|
|
119
|
+
"symbols": _count_block(base.symbol_count, head.symbol_count),
|
|
120
|
+
"endpoints": _count_block(base.endpoint_count, head.endpoint_count),
|
|
121
|
+
"dependency_edges": _count_block(
|
|
122
|
+
base.dependency_edge_count, head.dependency_edge_count
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
"endpoint_surface": {
|
|
126
|
+
"added_count": len(added),
|
|
127
|
+
"removed_count": len(removed),
|
|
128
|
+
"added": added[:_LIST_CAP],
|
|
129
|
+
"removed": removed[:_LIST_CAP],
|
|
130
|
+
},
|
|
131
|
+
"fan_in_shifts": {
|
|
132
|
+
"changed_count": len(shifts),
|
|
133
|
+
"shifts": shifts[:_LIST_CAP],
|
|
134
|
+
},
|
|
135
|
+
"provenance": (
|
|
136
|
+
"architectural_delta (D1): measured deltas derived from the CIR of two "
|
|
137
|
+
"snapshots. No breaking/non-breaking classification (D2), no verdict, "
|
|
138
|
+
"no ROI — the reviewer weighs the numbers."
|
|
139
|
+
),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def architectural_delta(
|
|
144
|
+
base_cir: "CanonicalRepositoryIR", head_cir: "CanonicalRepositoryIR"
|
|
145
|
+
) -> dict:
|
|
146
|
+
"""Convenience: extract both snapshots and diff them."""
|
|
147
|
+
return diff_metrics(extract_metrics(base_cir), extract_metrics(head_cir))
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""change_plan.py — D3 Architectural planning artifact (no code generation).
|
|
2
|
+
|
|
3
|
+
Engineering Decision Support, Part B / D3. Given a change target, emits a
|
|
4
|
+
deterministic, code-free PLAN: what a change to that symbol touches — the affected
|
|
5
|
+
components (reverse-dependency closure), the tests that cover them, the endpoints
|
|
6
|
+
whose contract is in scope, the rollback surface (files involved), and a review
|
|
7
|
+
checklist. All of it is a PROJECTION of the graph + endpoint surface + test-path
|
|
8
|
+
membership.
|
|
9
|
+
|
|
10
|
+
Moat boundary (roadmap): this lists what to REVIEW; it emits no code, no
|
|
11
|
+
implementation sequence beyond dependency depth, and no recommendation. Every item
|
|
12
|
+
is a deterministic consequence of the model — "these components depend on X", not
|
|
13
|
+
"you should change X this way". The engineer/agent decides and writes.
|
|
14
|
+
|
|
15
|
+
Determinism: reads the CIR only; a bounded reverse-BFS with a fixed node cap and a
|
|
16
|
+
stable ordering → same repo + same target → same plan.
|
|
17
|
+
|
|
18
|
+
Reuse: target resolution + the endpoint blast come from `project_blast_radius`
|
|
19
|
+
(the same machinery `impact-chain`/`pr-impact` use, so the plan is consistent with
|
|
20
|
+
them). The affected-component / test / rollback views are derived from an uncapped
|
|
21
|
+
(node-bounded) reverse-dependency closure over the SAME reverse graph, using the
|
|
22
|
+
canonical `_all_callers_from_rg` traversal (skips structural containment + imports,
|
|
23
|
+
normalizes DI-injection owners).
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
from sourcecode.canonical_ir import project_blast_radius
|
|
30
|
+
from sourcecode.repository_ir import _all_callers_from_rg
|
|
31
|
+
from sourcecode.security_posture import _graph_nodes
|
|
32
|
+
from sourcecode.path_filters import is_test_path
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from sourcecode.canonical_ir import CanonicalRepositoryIR
|
|
36
|
+
|
|
37
|
+
CHANGE_PLAN_SCHEMA: str = "change-plan-v1"
|
|
38
|
+
|
|
39
|
+
_LIST_CAP: int = 100
|
|
40
|
+
# Safety bound on the reverse closure so a hub target cannot blow up the traversal.
|
|
41
|
+
_NODE_CAP: int = 4000
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _dependents_closure(
|
|
45
|
+
reverse_graph: dict, seeds: list[str], max_depth: int, node_cap: int
|
|
46
|
+
) -> dict[str, int]:
|
|
47
|
+
"""Bounded reverse-dependency closure: dependant FQN → shallowest depth.
|
|
48
|
+
|
|
49
|
+
BFS over the reverse graph via the canonical `_all_callers_from_rg` traversal
|
|
50
|
+
(so containment/imports are excluded and DI owners normalized, exactly as
|
|
51
|
+
blast-radius/impact-chain compute reach). Seeds themselves are removed."""
|
|
52
|
+
affected: dict[str, int] = {}
|
|
53
|
+
seen: set[str] = set(seeds)
|
|
54
|
+
queue: list[tuple[str, int]] = [(s, 0) for s in seeds]
|
|
55
|
+
while queue and len(affected) < node_cap:
|
|
56
|
+
fqn, depth = queue.pop(0)
|
|
57
|
+
if depth >= max_depth:
|
|
58
|
+
continue
|
|
59
|
+
for dep in _all_callers_from_rg(fqn, reverse_graph):
|
|
60
|
+
if dep not in seen:
|
|
61
|
+
seen.add(dep)
|
|
62
|
+
affected[dep] = depth + 1
|
|
63
|
+
queue.append((dep, depth + 1))
|
|
64
|
+
for s in seeds:
|
|
65
|
+
affected.pop(s, None)
|
|
66
|
+
return affected
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _node_meta(cir: "CanonicalRepositoryIR") -> dict[str, dict]:
|
|
70
|
+
"""fqn → {file, role, kind} for every graph node."""
|
|
71
|
+
out: dict[str, dict] = {}
|
|
72
|
+
for n in _graph_nodes(cir):
|
|
73
|
+
fqn = n.get("fqn")
|
|
74
|
+
if fqn:
|
|
75
|
+
out[str(fqn)] = {
|
|
76
|
+
"file": str(n.get("source_file") or ""),
|
|
77
|
+
"role": str(n.get("role") or "other"),
|
|
78
|
+
"kind": str(n.get("symbol_kind") or n.get("type") or ""),
|
|
79
|
+
}
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_change_plan(
|
|
84
|
+
cir: "CanonicalRepositoryIR", target: str, *, max_depth: int = 4
|
|
85
|
+
) -> dict:
|
|
86
|
+
"""Build the deterministic change plan for `target` over `cir`."""
|
|
87
|
+
blast = project_blast_radius(cir, target, max_depth=max_depth)
|
|
88
|
+
resolution = blast.get("resolution", "not_found")
|
|
89
|
+
|
|
90
|
+
# Unresolvable target → return a plan-shaped resolution notice (never crash).
|
|
91
|
+
if resolution in ("not_found", "ambiguous_path") or not blast.get("matched_fqns"):
|
|
92
|
+
return {
|
|
93
|
+
"schema": CHANGE_PLAN_SCHEMA,
|
|
94
|
+
"target": target,
|
|
95
|
+
"resolution": resolution,
|
|
96
|
+
"message": blast.get("message", f"Target {target!r} not found."),
|
|
97
|
+
"candidates": blast.get("candidates", []),
|
|
98
|
+
"affected_components": {"count": 0, "components": []},
|
|
99
|
+
"covering_tests": {"count": 0, "tests": []},
|
|
100
|
+
"affected_endpoints": {"count": 0, "endpoints": []},
|
|
101
|
+
"rollback_surface": {"file_count": 0, "files": []},
|
|
102
|
+
"review_checklist": [],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
seeds = list(blast["matched_fqns"])
|
|
106
|
+
meta = _node_meta(cir)
|
|
107
|
+
affected = _dependents_closure(cir.reverse_graph or {}, seeds, max_depth, _NODE_CAP)
|
|
108
|
+
|
|
109
|
+
prod: list[dict] = []
|
|
110
|
+
tests: list[dict] = []
|
|
111
|
+
files: set[str] = set()
|
|
112
|
+
for fqn, depth in affected.items():
|
|
113
|
+
m = meta.get(fqn, {})
|
|
114
|
+
f = m.get("file", "")
|
|
115
|
+
files.add(f)
|
|
116
|
+
entry = {"symbol": fqn, "role": m.get("role", "other"),
|
|
117
|
+
"file": f, "depth": depth}
|
|
118
|
+
if is_test_path(f):
|
|
119
|
+
tests.append({"symbol": fqn, "file": f})
|
|
120
|
+
else:
|
|
121
|
+
prod.append(entry)
|
|
122
|
+
# Seed files are part of the rollback surface too (the target is being changed).
|
|
123
|
+
for s in seeds:
|
|
124
|
+
files.add(meta.get(s, {}).get("file", ""))
|
|
125
|
+
files.discard("")
|
|
126
|
+
|
|
127
|
+
prod.sort(key=lambda e: (e["depth"], e["symbol"]))
|
|
128
|
+
tests.sort(key=lambda e: e["symbol"])
|
|
129
|
+
|
|
130
|
+
endpoints_affected = blast.get("endpoints_affected", []) or []
|
|
131
|
+
ep_strings = sorted(
|
|
132
|
+
f"{e.get('method', '')} {e.get('path', '')}".strip()
|
|
133
|
+
for e in endpoints_affected if isinstance(e, dict)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
rollback_files = sorted(files)
|
|
137
|
+
|
|
138
|
+
# Review checklist — structural facts phrased as review items, never advice.
|
|
139
|
+
checklist: list[str] = []
|
|
140
|
+
checklist.append(
|
|
141
|
+
f"Review {len(prod)} affected production component(s) reachable from the change."
|
|
142
|
+
)
|
|
143
|
+
if ep_strings:
|
|
144
|
+
checklist.append(
|
|
145
|
+
f"Re-verify the contract of {len(ep_strings)} endpoint(s) in the blast radius."
|
|
146
|
+
)
|
|
147
|
+
if tests:
|
|
148
|
+
checklist.append(
|
|
149
|
+
f"Run {len(tests)} covering test symbol(s) that reach the change."
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
checklist.append(
|
|
153
|
+
"No covering tests reach the change — no existing test exercises this blast radius."
|
|
154
|
+
)
|
|
155
|
+
checklist.append(f"Rollback touches {len(rollback_files)} file(s).")
|
|
156
|
+
modules = blast.get("cross_module_impact") or []
|
|
157
|
+
if modules:
|
|
158
|
+
checklist.append(f"Change spans {len(modules)} module/subsystem(s).")
|
|
159
|
+
sec = blast.get("security_surface_affected") or []
|
|
160
|
+
if sec:
|
|
161
|
+
checklist.append(
|
|
162
|
+
f"{len(sec)} secured endpoint(s) in scope — re-check authorization."
|
|
163
|
+
)
|
|
164
|
+
txn = blast.get("transactional_boundaries_touched") or []
|
|
165
|
+
if txn:
|
|
166
|
+
checklist.append(f"{len(txn)} transactional boundary/boundaries in scope.")
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"schema": CHANGE_PLAN_SCHEMA,
|
|
170
|
+
"target": target,
|
|
171
|
+
"resolution": resolution,
|
|
172
|
+
"matched_symbols": sorted(seeds),
|
|
173
|
+
"affected_components": {
|
|
174
|
+
"count": len(prod),
|
|
175
|
+
"components": prod[:_LIST_CAP],
|
|
176
|
+
},
|
|
177
|
+
"covering_tests": {
|
|
178
|
+
"count": len(tests),
|
|
179
|
+
"tests": tests[:_LIST_CAP],
|
|
180
|
+
},
|
|
181
|
+
"affected_endpoints": {
|
|
182
|
+
"count": len(ep_strings),
|
|
183
|
+
"endpoints": ep_strings[:_LIST_CAP],
|
|
184
|
+
},
|
|
185
|
+
"rollback_surface": {
|
|
186
|
+
"file_count": len(rollback_files),
|
|
187
|
+
"files": rollback_files[:_LIST_CAP],
|
|
188
|
+
},
|
|
189
|
+
"review_checklist": checklist,
|
|
190
|
+
"provenance": (
|
|
191
|
+
"change_plan (D3): deterministic projection of the reverse-dependency "
|
|
192
|
+
"graph + endpoint surface + test-path membership. Lists what to review; "
|
|
193
|
+
"emits no code, no sequence beyond dependency depth, no recommendation."
|
|
194
|
+
),
|
|
195
|
+
}
|