sourcecode 2.6.3__tar.gz → 2.6.5__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.6.3 → sourcecode-2.6.5}/CHANGELOG.md +65 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/PKG-INFO +1 -1
- {sourcecode-2.6.3 → sourcecode-2.6.5}/pyproject.toml +1 -1
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/__init__.py +1 -1
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/cli.py +71 -3
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/repository_ir.py +15 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_impact.py +1 -1
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_tx_analyzer.py +141 -4
- {sourcecode-2.6.3 → sourcecode-2.6.5}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/.gitignore +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/.ruff.toml +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/CONTRIBUTING.md +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/LICENSE +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/README.md +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/SECURITY.md +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/raw +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/scripts/perf_harness.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/archetype.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/architectural_baseline.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/architectural_delta.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/change_plan.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/classifier.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/compare.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/constraint_diff.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/contract_diff.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/explain.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/filter_surface.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/license.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/parse_cache.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/perf.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/serializer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/verify_edit.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/verify_rules.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/functions/README.md +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.6.3 → sourcecode-2.6.5}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.6.5] — 2026-07-24
|
|
4
|
+
|
|
5
|
+
**Mistyped-subcommand ergonomics: `ask` no longer swallows a typo'd command as a path
|
|
6
|
+
and then blames the wrong token.** CLI-only; no analysis behaviour changes.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **A mistyped command now names the token the user got wrong, and suggests the real
|
|
11
|
+
command.** `ask` accepts a bare repository path (`ask ./repo`) as an implicit scan, so
|
|
12
|
+
a misspelled subcommand used to be swallowed as a path and surface a misleading error
|
|
13
|
+
about the *next* token — `ask notacommand /tmp` complained `No such command '/tmp'` (the
|
|
14
|
+
path), and `ask spring-audi` complained the directory did not exist. Now:
|
|
15
|
+
- a first positional followed by another non-subcommand positional
|
|
16
|
+
(`ask notacommand /tmp`) is rejected as an unknown command (exit `2`), naming the
|
|
17
|
+
first token and — when it is close to a real command — suggesting it;
|
|
18
|
+
- a lone token that does not resolve to a directory but closely matches a command
|
|
19
|
+
(`ask spring-audi`) adds a *did you mean `ask spring-audit`* hint to the path error.
|
|
20
|
+
|
|
21
|
+
The implicit-scan contract is unchanged: a single positional is still treated as a
|
|
22
|
+
repository path with existence validated downstream, and the documented
|
|
23
|
+
`ask <subcommand> <path>` order (and the `ask <path> <subcommand>` rejection) are
|
|
24
|
+
untouched.
|
|
25
|
+
|
|
26
|
+
## [2.6.4] — 2026-07-24
|
|
27
|
+
|
|
28
|
+
**TX-006: self-invocation proxy-bypass detection — a new Spring transaction rule that
|
|
29
|
+
also participates automatically in the `verify-edit` write-loop gate.** Plus a root-cause
|
|
30
|
+
false-positive fix in body-fact extraction and one internal-id output leak removed. No
|
|
31
|
+
change to any existing command's contract. Suite: **3742 passed, 5 skipped, 1 deselected**
|
|
32
|
+
(the deselected chunker test is a pre-existing external-fixture flake).
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **TX-006 — self-invocation of a `@Transactional` sibling (Spring proxy bypass).** A
|
|
37
|
+
same-instance call — an unqualified `foo()` or an explicit `this.foo()` to a method of
|
|
38
|
+
the same class — does not pass through the Spring proxy, so the target's method-level
|
|
39
|
+
`@Transactional` is silently ignored at runtime (the Sonar S2229 class of bug). Detection
|
|
40
|
+
is structural and vendor-agnostic, reading the Semantic-IR `invocation` atoms already
|
|
41
|
+
produced by the pipeline: the receiver is `None`/`this` and the call resolves to an
|
|
42
|
+
unambiguous same-class sibling that carries a method-level `@Transactional`. False-positive
|
|
43
|
+
guards are built in — overloaded/ambiguous targets are skipped, class-level-only
|
|
44
|
+
`@Transactional` is not flagged (the proxy already covers the outer entry method), and the
|
|
45
|
+
legitimate self-injection escape hatch (`self.foo()` through an injected same-type field)
|
|
46
|
+
uses a field receiver and never resolves as a sibling. Severity is tiered: `high` only when
|
|
47
|
+
the callee propagation mandates a distinct boundary (`REQUIRES_NEW`/`NESTED`/`NOT_SUPPORTED`/
|
|
48
|
+
`NEVER`/`MANDATORY`); a plain `REQUIRED`/`SUPPORTS` callee or a `readOnly` getter is `medium`.
|
|
49
|
+
Surfaced by `ask spring-audit` and, with no additional wiring, by the `verify-edit`
|
|
50
|
+
`tx_boundary_changed` axis — an edit that *introduces* a self-invocation now breaks the
|
|
51
|
+
write-loop verdict before commit, while a pre-existing one never fails an unrelated change.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **Chained-call receivers no longer read as self-calls (body-fact extraction).** A
|
|
56
|
+
method-chained invocation such as `Context.getBean().method()` left the receiver empty
|
|
57
|
+
because the receiver is an expression, not a bare identifier — causing it to be mistaken
|
|
58
|
+
for an unqualified self-call. A callee immediately preceded by `.` is now marked as a
|
|
59
|
+
non-self expression receiver. This is a general sibling-resolution correctness fix
|
|
60
|
+
(it also prevents a chained sibling-named call from being mis-resolved to a call edge);
|
|
61
|
+
`body_facts` is not a `cir_hash` input, so the fix is hash-neutral. It removed roughly
|
|
62
|
+
half of the raw TX-006 candidates on a real Spring codebase — all of them true bypass
|
|
63
|
+
false-positives, including a service that correctly routes `REQUIRES_NEW` through a
|
|
64
|
+
service-locator proxy.
|
|
65
|
+
- **Internal bug id removed from user-facing output.** The self-referential-exclusion
|
|
66
|
+
warning in `impact` no longer prints the internal `(BUG #2)` tag; the explanation stays.
|
|
67
|
+
|
|
3
68
|
## [2.6.3] — 2026-07-24
|
|
4
69
|
|
|
5
70
|
**`ask verify-edit` — the in-loop semantic diff gate lands (the read-context → write-loop
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import json
|
|
5
|
+
import difflib
|
|
5
6
|
import os
|
|
6
7
|
import sys
|
|
7
8
|
import threading
|
|
@@ -331,6 +332,54 @@ def _reject_path_before_subcommand(path_token: str, subcommand: str) -> "NoRetur
|
|
|
331
332
|
raise SystemExit(2)
|
|
332
333
|
|
|
333
334
|
|
|
335
|
+
def _reject_unknown_command(token: str) -> "NoReturn":
|
|
336
|
+
"""Refuse a first positional that is neither a known subcommand nor a real path.
|
|
337
|
+
|
|
338
|
+
`ask` accepts a bare repository path (`ask ./repo`) as an implicit scan, so a
|
|
339
|
+
mistyped subcommand used to be swallowed as a path and then surfaced a
|
|
340
|
+
misleading error naming the *wrong* token — `ask spring-audi` complained the
|
|
341
|
+
directory 'spring-audi' did not exist, and `ask notacommand /tmp` complained
|
|
342
|
+
'No such command /tmp' (the path, not the typo). Name the token the user
|
|
343
|
+
actually got wrong, and suggest the closest real commands.
|
|
344
|
+
"""
|
|
345
|
+
matches = difflib.get_close_matches(token, sorted(_SUBCOMMANDS), n=3, cutoff=0.6)
|
|
346
|
+
first = f"error: '{token}' is not a known command"
|
|
347
|
+
if matches:
|
|
348
|
+
first += f". Did you mean: {', '.join(matches)}?"
|
|
349
|
+
else:
|
|
350
|
+
first += " and is not an existing directory to scan."
|
|
351
|
+
print(
|
|
352
|
+
first + "\n run 'ask --help' to list commands, or pass an existing "
|
|
353
|
+
"repository path.",
|
|
354
|
+
file=sys.stderr,
|
|
355
|
+
)
|
|
356
|
+
raise SystemExit(2)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _has_trailing_nonsubcommand_positional(args: list[str], after: int) -> bool:
|
|
360
|
+
"""True when a bare positional that is NOT a subcommand appears after index
|
|
361
|
+
``after`` (options and the values they consume are skipped).
|
|
362
|
+
|
|
363
|
+
The implicit scan (`ask <path>`) owns exactly one positional, and a valid
|
|
364
|
+
invocation with two positionals always starts with a subcommand. So a first
|
|
365
|
+
non-subcommand positional followed by another non-subcommand positional —
|
|
366
|
+
`ask notacommand /tmp` — is malformed: the user meant a command. A trailing
|
|
367
|
+
token that IS a subcommand is left for ``_reject_path_before_subcommand``."""
|
|
368
|
+
skip_next = False
|
|
369
|
+
for arg in args[after + 1:]:
|
|
370
|
+
if skip_next:
|
|
371
|
+
skip_next = False
|
|
372
|
+
continue
|
|
373
|
+
if arg.startswith("-"):
|
|
374
|
+
if arg.split("=")[0] in _OPTIONS_WITH_VALUE and "=" not in arg:
|
|
375
|
+
skip_next = True
|
|
376
|
+
continue
|
|
377
|
+
if arg in _SUBCOMMANDS:
|
|
378
|
+
return False # wrong-order path/subcommand — handled elsewhere
|
|
379
|
+
return True
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
|
|
334
383
|
def _preprocess_args(args: list[str]) -> list[str]:
|
|
335
384
|
"""Extract a repository path token from an args list and store it in _detected_path.
|
|
336
385
|
|
|
@@ -363,7 +412,13 @@ def _preprocess_args(args: list[str]) -> list[str]:
|
|
|
363
412
|
return result # known subcommand — leave for Click to dispatch
|
|
364
413
|
if _path_index >= 0:
|
|
365
414
|
continue # a later positional is the subcommand's own business
|
|
366
|
-
# First genuine positional
|
|
415
|
+
# First genuine positional. `ask` treats it as the repository path to scan
|
|
416
|
+
# (existence is validated downstream, not here). But if it is followed by
|
|
417
|
+
# another non-subcommand positional, the one-path scan cannot own both —
|
|
418
|
+
# `ask notacommand /tmp` — so the user meant a command: name the token they
|
|
419
|
+
# got wrong instead of swallowing it and erroring on the next one.
|
|
420
|
+
if _has_trailing_nonsubcommand_positional(result, i):
|
|
421
|
+
_reject_unknown_command(arg)
|
|
367
422
|
_set_detected_path(arg)
|
|
368
423
|
_path_index = i
|
|
369
424
|
if _path_index >= 0:
|
|
@@ -1278,11 +1333,23 @@ def main(
|
|
|
1278
1333
|
_raw_path_input = _get_detected_path()
|
|
1279
1334
|
target = Path(_raw_path_input).resolve()
|
|
1280
1335
|
if not target.exists():
|
|
1336
|
+
# A non-existent path that closely matches a command name is almost always a
|
|
1337
|
+
# mistyped subcommand (`ask spring-audi`) swallowed as an implicit-scan path,
|
|
1338
|
+
# not a real directory. Point at the likely command instead of only the path.
|
|
1339
|
+
_cmd_matches = difflib.get_close_matches(
|
|
1340
|
+
_raw_path_input, sorted(_SUBCOMMANDS), n=3, cutoff=0.6
|
|
1341
|
+
)
|
|
1342
|
+
_hint = "Pass an existing repository directory."
|
|
1343
|
+
if _cmd_matches:
|
|
1344
|
+
_hint = (
|
|
1345
|
+
f"If you meant a command, try: {', '.join('ask ' + m for m in _cmd_matches)}. "
|
|
1346
|
+
"Otherwise pass an existing repository directory."
|
|
1347
|
+
)
|
|
1281
1348
|
_emit_error_json(
|
|
1282
1349
|
INVALID_INPUT_CODE,
|
|
1283
1350
|
f"Directory '{_raw_path_input}' does not exist.",
|
|
1284
1351
|
path=_raw_path_input,
|
|
1285
|
-
hint=
|
|
1352
|
+
hint=_hint,
|
|
1286
1353
|
expected="An existing directory path.",
|
|
1287
1354
|
)
|
|
1288
1355
|
raise typer.Exit(code=1)
|
|
@@ -5528,7 +5595,7 @@ def spring_audit_cmd(
|
|
|
5528
5595
|
help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
|
|
5529
5596
|
),
|
|
5530
5597
|
) -> None:
|
|
5531
|
-
"""Spring semantic audit: TX anomalies (TX-001..
|
|
5598
|
+
"""Spring semantic audit: TX anomalies (TX-001..006) + security surface (SEC-001..003).
|
|
5532
5599
|
|
|
5533
5600
|
\b
|
|
5534
5601
|
Detects:
|
|
@@ -5537,6 +5604,7 @@ def spring_audit_cmd(
|
|
|
5537
5604
|
TX-003 readOnly=true boundary propagating to write operation
|
|
5538
5605
|
TX-004 NOT_SUPPORTED/NEVER within active TX chain
|
|
5539
5606
|
TX-005 Exception swallowing inside @Transactional
|
|
5607
|
+
TX-006 Self-invocation of @Transactional sibling (proxy bypass)
|
|
5540
5608
|
SEC-001 Unsecured endpoint in annotation_based security model
|
|
5541
5609
|
SEC-002 CVE-2025-41248: @PreAuthorize on inherited method from generic supertype
|
|
5542
5610
|
SEC-003 @Transactional on @Controller/@RestController (TX in wrong layer)
|
|
@@ -2098,6 +2098,21 @@ def _extract_body_facts(
|
|
|
2098
2098
|
receiver, callee = m.group(1), m.group(2)
|
|
2099
2099
|
if callee in _CALL_KEYWORDS:
|
|
2100
2100
|
continue
|
|
2101
|
+
if receiver is None:
|
|
2102
|
+
# A chained/qualified call — `expr().foo()`, `getBean().foo()`,
|
|
2103
|
+
# `a.b().foo()` — leaves the receiver group empty because the
|
|
2104
|
+
# receiver is not a bare identifier. It must NOT be mistaken for an
|
|
2105
|
+
# unqualified self-call: if the callee is immediately preceded by '.'
|
|
2106
|
+
# the call is qualified (goes through whatever the preceding
|
|
2107
|
+
# expression returns, e.g. a Spring proxy), so mark it as a
|
|
2108
|
+
# non-self expression receiver. (openmrs OrderServiceImpl routes
|
|
2109
|
+
# REQUIRES_NEW through Context.getOrderService()....() precisely to
|
|
2110
|
+
# keep the proxy — that is correct code, not a self-invocation.)
|
|
2111
|
+
j = m.start() - 1
|
|
2112
|
+
while j >= 0 and body[j] in " \t\r\n":
|
|
2113
|
+
j -= 1
|
|
2114
|
+
if j >= 0 and body[j] == ".":
|
|
2115
|
+
receiver = "<expr>"
|
|
2101
2116
|
resolved: Optional[str] = None
|
|
2102
2117
|
if receiver in (None, "this") and callee in sib:
|
|
2103
2118
|
cands = [f for f in sib[callee] if f != caller.symbol]
|
|
@@ -1036,7 +1036,7 @@ class ImpactOrchestrator:
|
|
|
1036
1036
|
)
|
|
1037
1037
|
else:
|
|
1038
1038
|
warnings.append(
|
|
1039
|
-
f"Self-referential exclusion
|
|
1039
|
+
f"Self-referential exclusion: {self_excluded} member(s) of the "
|
|
1040
1040
|
f"analyzed class were dropped from callers — a class's own methods are "
|
|
1041
1041
|
f"members, not external callers (they do not count toward blast radius)."
|
|
1042
1042
|
)
|
|
@@ -6,9 +6,8 @@ Patterns implemented (Phase 2):
|
|
|
6
6
|
TX-003 readOnly=true boundary propagating into write-capable callee
|
|
7
7
|
TX-004 NOT_SUPPORTED or NEVER invoked from transactional call chain
|
|
8
8
|
TX-005 Exception swallowing inside @Transactional method (regex)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
requires AST-level analysis, regex produces too many false positives.
|
|
9
|
+
TX-006 Self-invocation of a @Transactional sibling — proxy bypass (Semantic-IR
|
|
10
|
+
invocation atoms; unambiguous same-class calls only)
|
|
12
11
|
|
|
13
12
|
All patterns are deterministic and never raise.
|
|
14
13
|
"""
|
|
@@ -683,6 +682,142 @@ class _TX005ExceptionSwallowing:
|
|
|
683
682
|
return blocks
|
|
684
683
|
|
|
685
684
|
|
|
685
|
+
# ---------------------------------------------------------------------------
|
|
686
|
+
# TX-006: self-invocation of a @Transactional sibling — proxy bypass
|
|
687
|
+
# ---------------------------------------------------------------------------
|
|
688
|
+
|
|
689
|
+
class _TX006SelfInvocation:
|
|
690
|
+
"""Same-instance call to a @Transactional sibling method bypasses the proxy.
|
|
691
|
+
|
|
692
|
+
Spring TX advice is applied by a proxy that wraps the bean. A call that does
|
|
693
|
+
NOT go through that proxy — an unqualified `foo()` or an explicit `this.foo()`
|
|
694
|
+
to a method of the SAME class — reaches the target directly, so its
|
|
695
|
+
method-level @Transactional is silently ignored at runtime (Sonar S2229).
|
|
696
|
+
|
|
697
|
+
Detection is structural and VAI-clean, reading the Semantic-IR invocation
|
|
698
|
+
atoms already produced by build_canonical_ir (cir.body_facts):
|
|
699
|
+
|
|
700
|
+
- the atom's receiver is None (unqualified) or "this" → not via a proxy
|
|
701
|
+
- resolved_fqn is set → unambiguous sibling
|
|
702
|
+
(the IR only resolves a sibling when receiver∈{None,"this"} and exactly one
|
|
703
|
+
same-class method matches — overloaded/ambiguous targets stay None → skipped)
|
|
704
|
+
- the resolved sibling carries a METHOD-LEVEL @Transactional → advice would apply
|
|
705
|
+
|
|
706
|
+
Class-level-only @Transactional is deliberately NOT flagged: the enclosing
|
|
707
|
+
proxy already applies it to the outer entry method, so the self-call runs in
|
|
708
|
+
that same transaction — no advice is lost. The self-injection workaround
|
|
709
|
+
(`self.foo()` where `self` is an injected same-type field) uses a named field
|
|
710
|
+
receiver, never None/"this", so it never resolves as a sibling → no false
|
|
711
|
+
positive on the legitimate escape hatch.
|
|
712
|
+
"""
|
|
713
|
+
pattern_id = "TX-006"
|
|
714
|
+
severity = "high" # nominal (Protocol); real severity is tiered per finding
|
|
715
|
+
|
|
716
|
+
# Propagations whose whole purpose is a distinct transaction boundary: a bypass
|
|
717
|
+
# demonstrably breaks them (a new/suspended/forbidden TX that never happens, or a
|
|
718
|
+
# runtime IllegalTransactionStateException). Everything else — a plain REQUIRED/
|
|
719
|
+
# SUPPORTS callee, or a readOnly getter (bypass loses only the read-only
|
|
720
|
+
# optimization, not correctness) — degrades to running in the caller's TX: a real
|
|
721
|
+
# bypass with lower blast radius → medium, matching TX-002/003 discipline.
|
|
722
|
+
_HIGH_IMPACT_PROPAGATION = frozenset({
|
|
723
|
+
"REQUIRES_NEW", "NESTED", "NOT_SUPPORTED", "NEVER", "MANDATORY",
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
def analyze(
|
|
727
|
+
self,
|
|
728
|
+
cir: "CanonicalRepositoryIR",
|
|
729
|
+
tx_index: TransactionBoundaryIndex,
|
|
730
|
+
root: Optional[Path],
|
|
731
|
+
*,
|
|
732
|
+
model: Optional[SpringSemanticModel] = None,
|
|
733
|
+
) -> list[SpringFinding]:
|
|
734
|
+
findings: list[SpringFinding] = []
|
|
735
|
+
body_facts = getattr(cir, "body_facts", {}) or {}
|
|
736
|
+
_seen_ids: set[str] = set()
|
|
737
|
+
|
|
738
|
+
for caller_fqn, atoms in body_facts.items():
|
|
739
|
+
if not isinstance(atoms, list):
|
|
740
|
+
continue
|
|
741
|
+
for atom in atoms:
|
|
742
|
+
if not isinstance(atom, dict) or atom.get("fact") != "invocation":
|
|
743
|
+
continue
|
|
744
|
+
# Only same-instance calls bypass the proxy: unqualified or `this.`.
|
|
745
|
+
if atom.get("receiver") not in (None, "this"):
|
|
746
|
+
continue
|
|
747
|
+
target = atom.get("resolved_fqn")
|
|
748
|
+
if not target:
|
|
749
|
+
continue # ambiguous/overloaded/unresolved sibling — honest skip
|
|
750
|
+
# The bypass only matters when the sibling declares its OWN
|
|
751
|
+
# method-level @Transactional; class-level advice already covers
|
|
752
|
+
# the outer entry method and is not lost by the self-call.
|
|
753
|
+
tb = tx_index.by_symbol.get(target)
|
|
754
|
+
if tb is None or tb.scope != "method":
|
|
755
|
+
continue
|
|
756
|
+
|
|
757
|
+
_fid = SpringFinding.make_id(self.pattern_id, f"{caller_fqn}→{target}")
|
|
758
|
+
if _fid in _seen_ids:
|
|
759
|
+
continue
|
|
760
|
+
_seen_ids.add(_fid)
|
|
761
|
+
|
|
762
|
+
caller_simple = caller_fqn.rsplit(".", 1)[-1].replace("#", ".")
|
|
763
|
+
callee_simple = target.rsplit(".", 1)[-1].replace("#", ".")
|
|
764
|
+
|
|
765
|
+
# Tier severity by what the bypass demonstrably breaks. A propagation
|
|
766
|
+
# that mandates a distinct boundary (or forbids an active TX) is
|
|
767
|
+
# high; a plain REQUIRED/SUPPORTS/readOnly callee degrades to the
|
|
768
|
+
# caller's TX — still a real bypass, lower blast radius → medium.
|
|
769
|
+
if tb.propagation in self._HIGH_IMPACT_PROPAGATION:
|
|
770
|
+
sev = "high"
|
|
771
|
+
else:
|
|
772
|
+
sev = "medium"
|
|
773
|
+
|
|
774
|
+
findings.append(SpringFinding(
|
|
775
|
+
id=_fid,
|
|
776
|
+
pattern_id=self.pattern_id,
|
|
777
|
+
category="tx",
|
|
778
|
+
severity=sev,
|
|
779
|
+
confidence="high",
|
|
780
|
+
title="Self-invocation of @Transactional method — Spring proxy bypass",
|
|
781
|
+
symbol=target,
|
|
782
|
+
source_file=tb.source_file,
|
|
783
|
+
evidence={
|
|
784
|
+
"caller": caller_fqn,
|
|
785
|
+
"callee": target,
|
|
786
|
+
"receiver": atom.get("receiver") or "(unqualified)",
|
|
787
|
+
"annotation": "@Transactional",
|
|
788
|
+
"callee_propagation": tb.propagation,
|
|
789
|
+
"callee_read_only": tb.read_only,
|
|
790
|
+
"proxy_mechanism": "proxy self-invocation",
|
|
791
|
+
"reason": (
|
|
792
|
+
"same-instance call does not pass through the Spring proxy"
|
|
793
|
+
),
|
|
794
|
+
},
|
|
795
|
+
explanation=(
|
|
796
|
+
f"{caller_simple} calls sibling {callee_simple} on the same "
|
|
797
|
+
"instance (unqualified / this.). The call does not pass through "
|
|
798
|
+
f"the Spring proxy, so the @Transactional on {callee_simple} "
|
|
799
|
+
f"(propagation={tb.propagation}"
|
|
800
|
+
f"{', readOnly=true' if tb.read_only else ''}) is silently "
|
|
801
|
+
"ignored — it executes in the caller's transaction context "
|
|
802
|
+
"(or none at all), not the one its annotation declares. "
|
|
803
|
+
"No new/independent transaction, propagation, or rollback boundary is applied."
|
|
804
|
+
),
|
|
805
|
+
fix_hint=(
|
|
806
|
+
"Move the @Transactional method to a separate Spring bean and "
|
|
807
|
+
"call it through the injected dependency, or inject the bean into "
|
|
808
|
+
"itself and call via the injected proxy reference, or use "
|
|
809
|
+
"TransactionTemplate for programmatic transaction control."
|
|
810
|
+
),
|
|
811
|
+
limitations=[
|
|
812
|
+
"Only unambiguous sibling calls are analyzed — overloaded targets "
|
|
813
|
+
"are reported as unresolved and skipped",
|
|
814
|
+
],
|
|
815
|
+
related_symbols=[target.split("#")[0]],
|
|
816
|
+
))
|
|
817
|
+
|
|
818
|
+
return findings
|
|
819
|
+
|
|
820
|
+
|
|
686
821
|
# ---------------------------------------------------------------------------
|
|
687
822
|
# Default pattern registry
|
|
688
823
|
# ---------------------------------------------------------------------------
|
|
@@ -693,6 +828,7 @@ _DEFAULT_TX_PATTERNS: list[TxPattern] = [
|
|
|
693
828
|
_TX003ReadOnlyWritePropagation(), # type: ignore[list-item]
|
|
694
829
|
_TX004TxSuspensionRisk(), # type: ignore[list-item]
|
|
695
830
|
_TX005ExceptionSwallowing(), # type: ignore[list-item]
|
|
831
|
+
_TX006SelfInvocation(), # type: ignore[list-item]
|
|
696
832
|
]
|
|
697
833
|
|
|
698
834
|
|
|
@@ -777,7 +913,8 @@ def run_tx_audit(
|
|
|
777
913
|
_spring_detected = tx_index.stats()["total"] > 0 or model.bean_graph.has_spring_beans()
|
|
778
914
|
|
|
779
915
|
_tx_limitations = [
|
|
780
|
-
"Self-invocation
|
|
916
|
+
"Self-invocation (this.method()) detected for unambiguous same-class calls "
|
|
917
|
+
"(TX-006); overloaded targets are reported as unresolved and skipped",
|
|
781
918
|
"Dynamic dispatch (interface/polymorphic calls) may produce incomplete call chains",
|
|
782
919
|
]
|
|
783
920
|
for _err in getattr(engine, "_last_analysis_errors", []):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|